Exemplo n.º 1
0
 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
Exemplo n.º 2
0
 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
Exemplo n.º 3
0
 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
Exemplo n.º 4
0
 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