def get_current_takes_for_payment(self, currency, weekly_amount): """ Return the list of current takes with the extra information that the `liberapay.payin.common.resolve_take_amounts` function needs to compute transfer amounts. """ takes = self.db.all( """ SELECT t.member , t.ctime , convert(t.amount, %(currency)s) AS amount , convert( coalesce_currency_amount(t.paid_in_advance, t.amount::currency), %(currency)s ) AS paid_in_advance , p.is_suspended FROM current_takes t JOIN participants p ON p.id = t.member WHERE t.team = %(team_id)s """, dict(currency=currency, team_id=self.id)) zero = Money.ZEROS[currency] income_amount = self.receiving.convert( currency) + weekly_amount.convert(currency) if income_amount == 0: income_amount = Money.MINIMUMS[currency] manual_takes_sum = MoneyBasket(t.amount for t in takes if t.amount > 0) n_auto_takes = sum(1 for t in takes if t.amount < 0) or 1 auto_take = ((income_amount - manual_takes_sum.fuzzy_sum(currency)) / n_auto_takes).round_up() if auto_take < 0: auto_take = zero for t in takes: t.amount = auto_take if t.amount < 0 else t.amount.convert( currency) return takes
def get_current_takes_for_payment(self, currency, provider, weekly_amount): """ Return the list of current takes with the extra information that the `liberapay.payin.common.resolve_take_amounts` function needs to compute transfer amounts. """ takes = self.db.all( """ SELECT t.member , t.ctime , convert(t.amount, %(currency)s) AS amount , convert( coalesce_currency_amount(t.paid_in_advance, t.amount::currency), %(currency)s ) AS paid_in_advance , p.is_suspended , ( CASE WHEN %(provider)s = 'mangopay' THEN p.mangopay_user_id IS NOT NULL ELSE EXISTS ( SELECT true FROM payment_accounts a WHERE a.participant = t.member AND a.provider = %(provider)s AND a.is_current AND a.verified AND coalesce(a.charges_enabled, true) ) END ) AS has_payment_account FROM current_takes t JOIN participants p ON p.id = t.member WHERE t.team = %(team_id)s """, dict(currency=currency, team_id=self.id, provider=provider)) zero = Money.ZEROS[currency] income_amount = self.receiving.convert( currency) + weekly_amount.convert(currency) if income_amount == 0: income_amount = Money.MINIMUMS[currency] manual_takes_sum = MoneyBasket(t.amount for t in takes if t.amount > 0) n_auto_takes = sum(1 for t in takes if t.amount < 0) or 1 auto_take = ((income_amount - manual_takes_sum.fuzzy_sum(currency)) / n_auto_takes).round_up() if auto_take < 0: auto_take = zero for t in takes: t.amount = auto_take if t.amount < 0 else t.amount.convert( currency) return takes
def get_current_takes_for_payment(self, currency, provider, weekly_amount): """ Return the list of current takes with the extra information that the `liberapay.payin.common.resolve_take_amounts` function needs to compute transfer amounts. """ takes = self.db.all(""" SELECT t.member , t.ctime , t.amount , (coalesce_currency_amount(( SELECT sum(pt.amount - coalesce(pt.reversed_amount, zero(pt.amount)), %(currency)s) FROM payin_transfers pt WHERE pt.recipient = t.member AND pt.team = t.team AND pt.context = 'team-donation' AND pt.status = 'succeeded' ), %(currency)s) + coalesce_currency_amount(( SELECT sum(tr.amount, %(currency)s) FROM transfers tr WHERE tr.tippee = t.member AND tr.team = t.team AND tr.context IN ('take', 'take-in-advance') AND tr.status = 'succeeded' AND tr.virtual IS NOT true ), %(currency)s)) AS received_sum , (coalesce_currency_amount(( SELECT sum(t2.amount, %(currency)s) FROM ( SELECT ( SELECT t2.amount FROM takes t2 WHERE t2.member = t.member AND t2.team = t.team AND t2.mtime < coalesce( payday.ts_start, current_timestamp ) ORDER BY t2.mtime DESC LIMIT 1 ) AS amount FROM paydays payday ) t2 WHERE t2.amount > 0 ), %(currency)s)) AS takes_sum , p.is_suspended , ( CASE WHEN %(provider)s = 'mangopay' THEN p.mangopay_user_id IS NOT NULL ELSE EXISTS ( SELECT true FROM payment_accounts a WHERE a.participant = t.member AND a.provider = %(provider)s AND a.is_current AND a.verified AND coalesce(a.charges_enabled, true) ) END ) AS has_payment_account FROM current_takes t JOIN participants p ON p.id = t.member WHERE t.team = %(team_id)s """, dict(currency=currency, team_id=self.id, provider=provider)) zero = Money.ZEROS[currency] income_amount = self.receiving.convert(currency) + weekly_amount.convert(currency) if income_amount == 0: income_amount = Money.MINIMUMS[currency] manual_takes_sum = MoneyBasket(t.amount for t in takes if t.amount > 0) auto_take = income_amount - manual_takes_sum.fuzzy_sum(currency) if auto_take < 0: auto_take = zero for t in takes: t.amount = auto_take if t.amount < 0 else t.amount.convert(currency) return takes
def resolve_takes(tips, takes, ref_currency): """Resolve many-to-many donations (team takes) """ total_income = MoneyBasket(t.full_amount for t in tips) if total_income == 0: return (), total_income takes = [t for t in takes if t.amount > 0] total_takes = MoneyBasket(t.amount for t in takes) if total_takes == 0: return (), total_income fuzzy_income_sum = total_income.fuzzy_sum(ref_currency) fuzzy_takes_sum = total_takes.fuzzy_sum(ref_currency) tips_by_currency = group_by(tips, lambda t: t.full_amount.currency) takes_by_preferred_currency = group_by(takes, lambda t: t.main_currency) takes_by_secondary_currency = {c: [] for c in tips_by_currency} takes_ratio = min(fuzzy_income_sum / fuzzy_takes_sum, 1) for take in takes: take.amount = (take.amount * takes_ratio).round_up() if take.paid_in_advance is None: take.paid_in_advance = take.amount.zero() if take.accepted_currencies is None: take.accepted_currencies = constants.CURRENCIES else: take.accepted_currencies = take.accepted_currencies.split(',') for accepted in take.accepted_currencies: skip = ( accepted == take.main_currency or accepted not in takes_by_secondary_currency ) if skip: continue takes_by_secondary_currency[accepted].append(take) tips_ratio = min(fuzzy_takes_sum / fuzzy_income_sum, 1) adjust_tips = tips_ratio != 1 if adjust_tips: # The team has a leftover, so donation amounts can be adjusted. # In the following loop we compute the "weeks" count of each tip. # For example the `weeks` value is 2.5 for a donation currently at # 10€/week which has distributed 25€ in the past. for tip in tips: tip.weeks = round_up(tip.past_transfers_sum / tip.full_amount) max_weeks = max(tip.weeks for tip in tips) min_weeks = min(tip.weeks for tip in tips) adjust_tips = max_weeks != min_weeks if adjust_tips: # Some donors have given fewer weeks worth of money than others, # we want to adjust the amounts so that the weeks count will # eventually be the same for every donation. min_tip_ratio = tips_ratio * Decimal('0.1') # Loop: compute how many "weeks" each tip is behind the "oldest" # tip, as well as a naive ratio and amount based on that number # of weeks for tip in tips: tip.weeks_to_catch_up = max_weeks - tip.weeks tip.ratio = min(min_tip_ratio + tip.weeks_to_catch_up, 1) tip.amount = (tip.full_amount * tip.ratio).round_up() naive_amounts_sum = MoneyBasket(tip.amount for tip in tips).fuzzy_sum(ref_currency) total_to_transfer = min(fuzzy_takes_sum, fuzzy_income_sum) delta = total_to_transfer - naive_amounts_sum if delta == 0: # The sum of the naive amounts computed in the previous loop # matches the end target, we got very lucky and no further # adjustments are required adjust_tips = False else: # Loop: compute the "leeway" of each tip, i.e. how much it # can be increased or decreased to fill the `delta` gap if delta < 0: # The naive amounts are too high: we want to lower the # amounts of the tips that have a "high" ratio, leaving # untouched the ones that are already low for tip in tips: if tip.ratio > min_tip_ratio: min_tip_amount = (tip.full_amount * min_tip_ratio).round_up() tip.leeway = min_tip_amount - tip.amount else: tip.leeway = tip.amount.zero() else: # The naive amounts are too low: we can raise all the # tips that aren't already at their maximum for tip in tips: tip.leeway = tip.full_amount - tip.amount leeway = MoneyBasket(tip.leeway for tip in tips).fuzzy_sum(ref_currency) if leeway == 0: # We don't actually have any leeway, give up adjust_tips = False else: leeway_ratio = min(delta / leeway, 1) tips = sorted(tips, key=lambda tip: (-tip.weeks_to_catch_up, tip.id)) # Loop: compute the adjusted donation amounts, and do the transfers transfers = [] for tip in tips: if tip.paid_in_advance is None: tip.paid_in_advance = tip.full_amount.zero() if adjust_tips: tip_amount = (tip.amount + tip.leeway * leeway_ratio).round_up() if tip_amount == 0: continue assert tip_amount > 0 assert tip_amount <= tip.full_amount tip.amount = tip_amount else: tip.amount = (tip.full_amount * tips_ratio).round_up() tip_currency = tip.amount.currency sorted_takes = chain( takes_by_preferred_currency.get(tip_currency, ()), takes_by_secondary_currency.get(tip_currency, ()) ) for take in sorted_takes: if take.amount == 0 or tip.tipper == take.member: continue fuzzy_take_amount = take.amount.convert(tip_currency) in_advance_amount = min( tip.amount, fuzzy_take_amount, max(tip.paid_in_advance, 0), max(take.paid_in_advance.convert(tip_currency), 0), ) on_time_amount = min( max(tip.amount - in_advance_amount, 0), max(fuzzy_take_amount - in_advance_amount, 0), tip.balances[tip_currency], ) transfer_amount = in_advance_amount + on_time_amount if transfer_amount == 0: continue transfers.append(TakeTransfer(tip.tipper, take.member, transfer_amount)) if transfer_amount == fuzzy_take_amount: take.amount = take.amount.zero() else: take.amount -= transfer_amount.convert(take.amount.currency) if in_advance_amount: tip.paid_in_advance -= in_advance_amount take.paid_in_advance -= in_advance_amount.convert(take.amount.currency) if on_time_amount: tip.balances -= on_time_amount tip.amount -= transfer_amount if tip.amount == 0: break leftover = total_income - MoneyBasket(t.amount for t in transfers) return transfers, leftover