<commit_before>"""Functions for moving money between Gratipay and the outside world.
"""
from __future__ import unicode_literals

from decimal import Decimal, ROUND_UP

import balanced

from aspen import log
from aspen.utils import typecheck
from gratipay.exceptions import NegativeBalance, NotWhitelisted
from gratipay.models import check_db
from gratipay.models.participant import Participant
from gratipay.models.exchange_route import ExchangeRoute


# Balanced has a $0.50 minimum. We go even higher to avoid onerous
# per-transaction fees. See:
# https://github.com/gratipay/gratipay.com/issues/167

MINIMUM_CHARGE = Decimal("9.41")
MINIMUM_CREDIT = Decimal("10.00")

FEE_CHARGE = ( Decimal("0.30")   # $0.30
             , Decimal("0.029")  #  2.9%
              )
FEE_CREDIT = Decimal("0.00")    # Balanced doesn't actually charge us for this,
                                # because we were in the door early enough.


def upcharge(amount):
    """Given an amount, return a higher amount and the difference.
    """
    typecheck(amount, Decimal)
    charge_amount = (amount + FEE_CHARGE[0]) / (1 - FEE_CHARGE[1])
    charge_amount = charge_amount.quantize(FEE_CHARGE[0], rounding=ROUND_UP)
    return charge_amount, charge_amount - amount

assert upcharge(MINIMUM_CHARGE) == (Decimal('10.00'), Decimal('0.59'))


def skim_credit(amount):
    """Given an amount, return a lower amount and the difference.
    """
    typecheck(amount, Decimal)
    return amount - FEE_CREDIT, FEE_CREDIT


def repr_exception(e):
    if isinstance(e, balanced.exc.HTTPError):
        return '%s %s, %s' % (e.status_code, e.status, e.description)
    else:
        return repr(e)


def ach_credit(db, participant, withhold, minimum_credit=MINIMUM_CREDIT):

    # Compute the amount to credit them.
    # ==================================
    # Leave money in Gratipay to cover their obligations next week (as these
    # currently stand).

    balance = participant.balance
    assert balance is not None, balance # sanity check
    amount = balance - withhold

    # Do some last-minute checks.
    # ===========================

    if amount <= 0:
        return      # Participant not owed anything.

    if amount < minimum_credit:
        also_log = ""
        if withhold > 0:
            also_log = " ($%s balance - $%s in obligations)"
            also_log %= (balance, withhold)
        log("Minimum payout is $%s. %s is only due $%s%s."
           % (minimum_credit, participant.username, amount, also_log))
        return      # Participant owed too little.

    if not participant.is_whitelisted:
        raise NotWhitelisted      # Participant not trusted.

    route = ExchangeRoute.from_network(participant, 'balanced-ba')
    if not route:
        return 'No bank account'


    # Do final calculations.
    # ======================

    credit_amount, fee = skim_credit(amount)
    cents = credit_amount * 100

    if withhold > 0:
        also_log = "$%s balance - $%s in obligations"
        also_log %= (balance, withhold)
    else:
        also_log = "$%s" % amount
    msg = "Crediting %s %d cents (%s - $%s fee = $%s) on Balanced ... "
    msg %= (participant.username, cents, also_log, fee, credit_amount)


    # Try to dance with Balanced.
    # ===========================

    e_id = record_exchange(db, route, -credit_amount, fee, participant, 'pre')
    meta = dict(exchange_id=e_id, participant_id=participant.id)
    try:
        ba = balanced.BankAccount.fetch(route.address)
        ba.credit(amount=cents, description=participant.username, meta=meta)
        record_exchange_result(db, e_id, 'pending', None, participant)
        log(msg + "succeeded.")
        error = ""
    except Exception as e:
        error = repr_exception(e)
        record_exchange_result(db, e_id, 'failed', error, participant)
        log(msg + "failed: %s" % error)

    return error


def create_card_hold(db, participant, amount):
    """Create a hold on the participant's credit card.

    Amount should be the nominal amount. We'll compute Gratipay's fee below
    this function and add it to amount to end up with charge_amount.

    """
    typecheck(amount, Decimal)

    username = participant.username


    # Perform some last-minute checks.
    # ================================

    if participant.is_suspicious is not False:
        raise NotWhitelisted      # Participant not trusted.

    route = ExchangeRoute.from_network(participant, 'balanced-cc')
    if not route:
        return None, 'No credit card'


    # Go to Balanced.
    # ===============

    cents, amount_str, charge_amount, fee = _prep_hit(amount)
    msg = "Holding " + amount_str + " on Balanced for " + username + " ... "

    hold = None
    try:
        card = balanced.Card.fetch(route.address)
        hold = card.hold( amount=cents
                        , description=username<commit_msg>reduce the number of Balanced API requests<commit_after>"""Functions for moving money between Gratipay and the outside world.
"""
from __future__ import unicode_literals

from decimal import Decimal, ROUND_UP

import balanced

from aspen import log
from aspen.utils import typecheck
from gratipay.exceptions import NegativeBalance, NotWhitelisted
from gratipay.models import check_db
from gratipay.models.participant import Participant
from gratipay.models.exchange_route import ExchangeRoute


BALANCED_CLASSES = {
    "bank_accounts": balanced.BankAccount,
    "cards": balanced.Card,
}

BALANCED_LINKS = {
    "bank_accounts": {
        "bank_accounts.bank_account_verification": "/verifications/{bank_accounts.bank_account_verification}",
        "bank_accounts.bank_account_verifications": "/bank_accounts/{bank_accounts.id}/verifications",
        "bank_accounts.credits": "/bank_accounts/{bank_accounts.id}/credits",
        "bank_accounts.customer": "/customers/{bank_accounts.customer}",
        "bank_accounts.debits": "/bank_accounts/{bank_accounts.id}/debits",
        "bank_accounts.settlements": "/bank_accounts/{bank_accounts.id}/settlements"
    },
    "cards": {
        "cards.card_holds": "/cards/{cards.id}/card_holds",
        "cards.customer": "/customers/{cards.customer}",
        "cards.debits": "/cards/{cards.id}/debits",
        "cards.disputes": "/cards/{cards.id}/disputes"
    }
}

def thing_from_href(things, href):
    """This is a temporary hack, we'll get rid of it when we migrate to Stripe.
    """
    id = href.rsplit('/', 1)[1]
    d = {'href': href, 'id': id, 'links': {}, 'meta': {}}
    C = BALANCED_CLASSES[things]
    return C(**{things: [d], 'links': BALANCED_LINKS[things]})


# Balanced has a $0.50 minimum. We go even higher to avoid onerous
# per-transaction fees. See:
# https://github.com/gratipay/gratipay.com/issues/167

MINIMUM_CHARGE = Decimal("9.41")
MINIMUM_CREDIT = Decimal("10.00")

FEE_CHARGE = ( Decimal("0.30")   # $0.30
             , Decimal("0.029")  #  2.9%
              )
FEE_CREDIT = Decimal("0.00")    # Balanced doesn't actually charge us for this,
                                # because we were in the door early enough.


def upcharge(amount):
    """Given an amount, return a higher amount and the difference.
    """
    typecheck(amount, Decimal)
    charge_amount = (amount + FEE_CHARGE[0]) / (1 - FEE_CHARGE[1])
    charge_amount = charge_amount.quantize(FEE_CHARGE[0], rounding=ROUND_UP)
    return charge_amount, charge_amount - amount

assert upcharge(MINIMUM_CHARGE) == (Decimal('10.00'), Decimal('0.59'))


def skim_credit(amount):
    """Given an amount, return a lower amount and the difference.
    """
    typecheck(amount, Decimal)
    return amount - FEE_CREDIT, FEE_CREDIT


def repr_exception(e):
    if isinstance(e, balanced.exc.HTTPError):
        return '%s %s, %s' % (e.status_code, e.status, e.description)
    else:
        return repr(e)


def ach_credit(db, participant, withhold, minimum_credit=MINIMUM_CREDIT):

    # Compute the amount to credit them.
    # ==================================
    # Leave money in Gratipay to cover their obligations next week (as these
    # currently stand).

    balance = participant.balance
    assert balance is not None, balance # sanity check
    amount = balance - withhold

    # Do some last-minute checks.
    # ===========================

    if amount <= 0:
        return      # Participant not owed anything.

    if amount < minimum_credit:
        also_log = ""
        if withhold > 0:
            also_log = " ($%s balance - $%s in obligations)"
            also_log %= (balance, withhold)
        log("Minimum payout is $%s. %s is only due $%s%s."
           % (minimum_credit, participant.username, amount, also_log))
        return      # Participant owed too little.

    if not participant.is_whitelisted:
        raise NotWhitelisted      # Participant not trusted.

    route = ExchangeRoute.from_network(participant, 'balanced-ba')
    if not route:
        return 'No bank account'


    # Do final calculations.
    # ======================

    credit_amount, fee = skim_credit(amount)
    cents = credit_amount * 100

    if withhold > 0:
        also_log = "$%s balance - $%s in obligations"
        also_log %= (balance, withhold)
    else:
        also_log = "$%s" % amount
    msg = "Crediting %s %d cents (%s - $%s fee = $%s) on Balanced ... "
    msg %= (participant.username, cents, also_log, fee, credit_amount)


    # Try to dance with Balanced.
    # ===========================

    e_id = record_exchange(db, route, -credit_amount, fee, participant, 'pre')
    meta = dict(exchange_id=e_id, participant_id=participant.id)
    try:
        ba = thing_from_href('bank_accounts', route.address)
        ba.credit(amount=cents, description=participant.username, meta=meta)
        record_exchange_result(db, e_id, 'pending', None, participant)
        log(msg + "succeeded.")
        error = ""
    except Exception as e:
        error = repr_exception(e)
        record_exchange_result(db, e_id, 'failed', error, participant)
        log(msg + "failed: %s" % error)

    return error


def create_card_hold(db, participant, amount):
    """Create a hold on the participant's credit card.

    Amount should be the nominal amount. We'll compute Gratipay's fee below
    this function and add it to amount to end up with charge_amount.

    """
    typecheck(amount, Decimal)

    username = participant.username


    # Perform some last-minute checks.
    # ================================

    if participant.is_suspicious is not False:
        raise NotWhitelisted      # Participant not trusted.

    route = ExchangeRoute.from_network(participant, 'balanced-cc')
    if not route:
        return None, 'No credit card'


    # Go to Balanced.
    # ===============

    cents, amount_str, charge_amount, fee = _prep_hit(amount)
    msg = "Holding " + amount_str + " on Balanced for " + username + " ... "

    hold = None
    try:
        card = thing_from_href('cards', route.address)
        hold = card.hold( amount=cents
                        , description=username
{'fraction_non_alphanumeric': 0.13367725586025886, 'fraction_numerical': 0.008326545388722961, 'mean_word_length': 3.018181818181818, 'pattern_counts': {'":': 14, '<': 7, '<?xml version=': 0, '>': 7, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3195183', 'n_tokens_mistral': 3546, 'n_tokens_neox': 3269, 'n_words': 928}