diff --git a/liberapay/constants.py b/liberapay/constants.py index a6c94326f2..29ed3f2930 100644 --- a/liberapay/constants.py +++ b/liberapay/constants.py @@ -309,10 +309,20 @@ def __missing__(self, currency): 'EUR': Money('40.00', 'EUR'), 'USD': Money('48.00', 'USD'), }), - 'max_acceptable': MoneyAutoConvertDict({ - 'EUR': Money('5000.00', 'EUR'), - 'USD': Money('5000.00', 'USD'), - }), + 'max_acceptable': { + 'new_donor': MoneyAutoConvertDict({ + 'EUR': Money('5200.00', 'EUR'), + 'USD': Money('5200.00', 'USD'), + }), + 'active_donor': MoneyAutoConvertDict({ + 'EUR': Money('12000.00', 'EUR'), + 'USD': Money('12000.00', 'USD'), + }), + 'trusted_donor': MoneyAutoConvertDict({ + 'EUR': Money('52000.00', 'EUR'), + 'USD': Money('52000.00', 'USD'), + }), + }, }, 'stripe': { 'min_acceptable': MoneyAutoConvertDict({ # fee > 10% @@ -327,10 +337,20 @@ def __missing__(self, currency): 'EUR': Money('40.00', 'EUR'), 'USD': Money('48.00', 'USD'), }), - 'max_acceptable': MoneyAutoConvertDict({ - 'EUR': Money('5000.00', 'EUR'), - 'USD': Money('5000.00', 'USD'), - }), + 'max_acceptable': { + 'new_donor': MoneyAutoConvertDict({ + 'EUR': Money('5200.00', 'EUR'), + 'USD': Money('5200.00', 'USD'), + }), + 'active_donor': MoneyAutoConvertDict({ + 'EUR': Money('12000.00', 'EUR'), + 'USD': Money('12000.00', 'USD'), + }), + 'trusted_donor': MoneyAutoConvertDict({ + 'EUR': Money('52000.00', 'EUR'), + 'USD': Money('52000.00', 'USD'), + }), + }, }, } diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py index 55f2f7de7a..0fb873b6a1 100644 --- a/liberapay/models/participant.py +++ b/liberapay/models/participant.py @@ -2685,7 +2685,7 @@ def find_partial_match(new_sp, current_schedule_map): else: tip.renewal_amount = None if not tip.renewal_amount or tip.renewal_amount < (tip.amount * 2): - pp = PayinProspect([tip], 'stripe') + pp = PayinProspect(self, [tip], 'stripe') tip.renewal_amount = pp.moderate_proposed_amount else: tip.renewal_amount = None diff --git a/liberapay/payin/prospect.py b/liberapay/payin/prospect.py index fbb5e97d85..b4f1b9c346 100644 --- a/liberapay/payin/prospect.py +++ b/liberapay/payin/prospect.py @@ -16,7 +16,7 @@ class PayinProspect: 'suggested_amounts', ) - def __init__(self, tips, provider): + def __init__(self, donor, tips, provider): """This method computes the suggested payment amounts. Args: @@ -60,8 +60,23 @@ def __init__(self, tips, provider): self.one_weeks_worth ) self.low_fee_amount = standard_amounts['low_fee'][self.currency] + standard_maximums = standard_amounts['max_acceptable'] + donor_has_at_least_one_good_payment = donor.db.one(""" + SELECT count(*) + FROM payins + WHERE payer = %s + AND status = 'succeeded' + AND refunded_amount IS NULL + AND ctime < (current_timestamp - interval '30 days') + """, (donor.id,)) > 0 self.max_acceptable_amount = min( - standard_amounts['max_acceptable'][self.currency], + ( + standard_maximums['trusted_donor'][self.currency] + if donor.is_suspended is False else + standard_maximums['active_donor'][self.currency] + if donor_has_at_least_one_good_payment else + standard_maximums['new_donor'][self.currency] + ), self.twenty_years_worth ) self.min_proposed_amount = min( diff --git a/tests/py/test_payins.py b/tests/py/test_payins.py index f1c2015ad2..31aa1113e8 100644 --- a/tests/py/test_payins.py +++ b/tests/py/test_payins.py @@ -184,7 +184,7 @@ def setUp(self): def test_minimum_weekly_EUR_tip(self): tip_amount = DONATION_LIMITS['EUR']['weekly'][0] tip = self.alice.set_tip_to(self.bob, tip_amount) - pp = PayinProspect([tip], 'stripe') + pp = PayinProspect(self.alice, [tip], 'stripe') assert pp.currency == 'EUR' assert pp.period == 'weekly' assert pp.one_periods_worth == tip_amount @@ -198,7 +198,7 @@ def test_minimum_weekly_EUR_tip(self): def test_minimum_monthly_EUR_tip(self): tip_amount = DONATION_LIMITS['EUR']['monthly'][0] tip = self.alice.set_tip_to(self.bob, tip_amount, period='monthly') - pp = PayinProspect([tip], 'stripe') + pp = PayinProspect(self.alice, [tip], 'stripe') assert pp.currency == 'EUR' assert pp.period == 'monthly' assert pp.one_periods_worth == tip_amount @@ -212,7 +212,7 @@ def test_minimum_monthly_EUR_tip(self): def test_minimum_yearly_EUR_tip(self): tip_amount = DONATION_LIMITS['EUR']['yearly'][0] tip = self.alice.set_tip_to(self.bob, tip_amount, period='yearly') - pp = PayinProspect([tip], 'stripe') + pp = PayinProspect(self.alice, [tip], 'stripe') assert pp.currency == 'EUR' assert pp.period == 'yearly' assert pp.one_periods_worth == tip_amount @@ -226,7 +226,7 @@ def test_minimum_yearly_EUR_tip(self): def test_small_weekly_USD_tip(self): tip_amount = STANDARD_TIPS['USD'][1].weekly tip = self.alice.set_tip_to(self.bob, tip_amount) - pp = PayinProspect([tip], 'stripe') + pp = PayinProspect(self.alice, [tip], 'stripe') assert pp.currency == 'USD' assert pp.period == 'weekly' assert pp.one_periods_worth == tip_amount @@ -239,7 +239,7 @@ def test_small_weekly_USD_tip(self): def test_small_monthly_USD_tip(self): tip_amount = USD('1.00') tip = self.alice.set_tip_to(self.bob, tip_amount, period='monthly') - pp = PayinProspect([tip], 'stripe') + pp = PayinProspect(self.alice, [tip], 'stripe') assert pp.currency == 'USD' assert pp.period == 'monthly' assert pp.one_periods_worth == tip_amount @@ -252,7 +252,7 @@ def test_small_monthly_USD_tip(self): def test_small_yearly_USD_tip(self): tip_amount = USD('10.00') tip = self.alice.set_tip_to(self.bob, tip_amount, period='yearly') - pp = PayinProspect([tip], 'stripe') + pp = PayinProspect(self.alice, [tip], 'stripe') assert pp.currency == 'USD' assert pp.period == 'yearly' assert pp.one_periods_worth == tip_amount @@ -267,7 +267,7 @@ def test_small_yearly_USD_tip(self): def test_medium_weekly_JPY_tip(self): tip_amount = STANDARD_TIPS['JPY'][2].weekly tip = self.alice.set_tip_to(self.bob, tip_amount) - pp = PayinProspect([tip], 'stripe') + pp = PayinProspect(self.alice, [tip], 'stripe') assert pp.currency == 'JPY' assert pp.period == 'weekly' assert pp.one_periods_worth == tip_amount @@ -282,7 +282,7 @@ def test_medium_weekly_JPY_tip(self): def test_medium_monthly_JPY_tip(self): tip_amount = JPY('500') tip = self.alice.set_tip_to(self.bob, tip_amount, period='monthly') - pp = PayinProspect([tip], 'stripe') + pp = PayinProspect(self.alice, [tip], 'stripe') assert pp.currency == 'JPY' assert pp.period == 'monthly' assert pp.one_periods_worth == tip_amount @@ -297,7 +297,7 @@ def test_medium_monthly_JPY_tip(self): def test_medium_yearly_JPY_tip(self): tip_amount = JPY('5000') tip = self.alice.set_tip_to(self.bob, tip_amount, period='yearly') - pp = PayinProspect([tip], 'stripe') + pp = PayinProspect(self.alice, [tip], 'stripe') assert pp.currency == 'JPY' assert pp.period == 'yearly' assert pp.one_periods_worth == tip_amount @@ -310,7 +310,7 @@ def test_medium_yearly_JPY_tip(self): def test_large_weekly_EUR_tip(self): tip_amount = STANDARD_TIPS['EUR'][3].weekly tip = self.alice.set_tip_to(self.bob, tip_amount) - pp = PayinProspect([tip], 'stripe') + pp = PayinProspect(self.alice, [tip], 'stripe') assert pp.currency == 'EUR' assert pp.period == 'weekly' assert pp.one_periods_worth == tip_amount @@ -324,7 +324,7 @@ def test_large_weekly_EUR_tip(self): def test_large_monthly_EUR_tip(self): tip_amount = EUR('25.00') tip = self.alice.set_tip_to(self.bob, tip_amount, period='monthly') - pp = PayinProspect([tip], 'stripe') + pp = PayinProspect(self.alice, [tip], 'stripe') assert pp.currency == 'EUR' assert pp.period == 'monthly' assert pp.one_periods_worth == tip_amount @@ -338,7 +338,7 @@ def test_large_monthly_EUR_tip(self): def test_large_yearly_EUR_tip(self): tip_amount = EUR('500.00') tip = self.alice.set_tip_to(self.bob, tip_amount, period='yearly') - pp = PayinProspect([tip], 'stripe') + pp = PayinProspect(self.alice, [tip], 'stripe') assert pp.currency == 'EUR' assert pp.period == 'yearly' assert pp.one_periods_worth == tip_amount @@ -350,20 +350,21 @@ def test_large_yearly_EUR_tip(self): def test_maximum_yearly_EUR_tip(self): tip_amount = EUR('5200.00') tip = self.alice.set_tip_to(self.bob, tip_amount, period='yearly') - pp = PayinProspect([tip], 'stripe') + pp = PayinProspect(self.alice, [tip], 'stripe') assert pp.currency == 'EUR' assert pp.period == 'yearly' assert pp.one_periods_worth == tip_amount assert pp.one_weeks_worth == EUR('100.00') assert pp.one_months_worth == EUR('433.33') assert pp.one_years_worth == tip_amount - assert pp.suggested_amounts == [EUR('5000.00')] + assert pp.suggested_amounts == [EUR('5200.00')] + assert pp.max_acceptable_amount == EUR('5200.00') def test_two_small_monthly_USD_tips(self): tip_amount = USD('1.00') tip1 = self.alice.set_tip_to(self.bob, tip_amount, period='monthly') tip2 = self.alice.set_tip_to(self.carl, tip_amount, period='monthly') - pp = PayinProspect([tip1, tip2], 'stripe') + pp = PayinProspect(self.alice, [tip1, tip2], 'stripe') assert pp.currency == 'USD' assert pp.period == 'monthly' assert pp.one_periods_worth == tip_amount * 2 @@ -379,7 +380,7 @@ def test_two_medium_yearly_KRW_tips(self): tip_amount = KRW('50000') tip1 = self.alice.set_tip_to(self.bob, tip_amount, period='yearly') tip2 = self.alice.set_tip_to(self.carl, tip_amount, period='yearly') - pp = PayinProspect([tip1, tip2], 'stripe') + pp = PayinProspect(self.alice, [tip1, tip2], 'stripe') assert pp.currency == 'KRW' assert pp.period == 'yearly' assert pp.one_periods_worth == tip_amount * 2 @@ -392,7 +393,7 @@ def test_two_medium_yearly_KRW_tips(self): def test_two_very_different_EUR_tips(self): tip1 = self.alice.set_tip_to(self.bob, EUR('0.24'), period='weekly') tip2 = self.alice.set_tip_to(self.carl, EUR('240.00'), period='yearly') - pp = PayinProspect([tip1, tip2], 'stripe') + pp = PayinProspect(self.alice, [tip1, tip2], 'stripe') assert pp.currency == 'EUR' assert pp.period == 'weekly' assert pp.one_periods_worth == EUR('4.86') @@ -408,7 +409,7 @@ def test_three_very_different_EUR_tips(self): tip1 = self.alice.set_tip_to(self.bob, EUR('0.01'), period='weekly') tip2 = self.alice.set_tip_to(self.carl, EUR('1.00'), period='monthly') tip3 = self.alice.set_tip_to(self.dana, EUR('5200.00'), period='yearly') - pp = PayinProspect([tip1, tip2, tip3], 'stripe') + pp = PayinProspect(self.alice, [tip1, tip2, tip3], 'stripe') assert pp.currency == 'EUR' assert pp.period == 'monthly' assert pp.one_periods_worth == EUR('434.38') @@ -616,7 +617,7 @@ def test_00_payin_stripe_card(self): self.db.run("ALTER SEQUENCE payins_id_seq RESTART WITH %s", (self.offset,)) self.db.run("ALTER SEQUENCE payin_transfers_id_seq RESTART WITH %s", (self.offset,)) self.add_payment_account(self.creator_1, 'stripe') - tip = self.donor.set_tip_to(self.creator_1, EUR('0.02')) + tip = self.donor.set_tip_to(self.creator_1, EUR('0.05')) # 1st request: test getting the payment page r = self.client.GET( diff --git a/www/%username/giving/pay/paypal/%payin_id.spt b/www/%username/giving/pay/paypal/%payin_id.spt index 2e2e613d87..59294b0a54 100644 --- a/www/%username/giving/pay/paypal/%payin_id.spt +++ b/www/%username/giving/pay/paypal/%payin_id.spt @@ -51,12 +51,9 @@ if request.method == 'POST': if len(tips) > 1: raise response.error(400, "We don't support one-to-many payments through PayPal yet.") - tips_weekly_sum = Money.sum((tip.amount for tip in tips), payin_currency) - amount_min = max( - constants.PAYIN_AMOUNTS['paypal']['min_acceptable'][payin_currency], - tips_weekly_sum - ) - amount_max = constants.PAYIN_AMOUNTS['paypal']['max_acceptable'][payin_currency] + prospect = PayinProspect(payer, tips, 'paypal') + amount_min = prospect.min_acceptable_amount + amount_max = prospect.max_acceptable_amount if payin_amount < amount_min or payin_amount > amount_max: raise response.error(400, _( "'{0}' is not an acceptable amount (min={1}, max={2})", @@ -118,7 +115,7 @@ if tippees: ] if len(set(tip.amount.currency for tip in tips)) != 1: raise response.invalid_input(tippees, 'beneficiary', 'querystring') - payment = PayinProspect(tips, 'paypal') + payment = PayinProspect(payer, tips, 'paypal') del tips elif not payin_id: diff --git a/www/%username/giving/pay/stripe/%payin_id.spt b/www/%username/giving/pay/stripe/%payin_id.spt index 771db4fe62..926d5bc758 100644 --- a/www/%username/giving/pay/stripe/%payin_id.spt +++ b/www/%username/giving/pay/stripe/%payin_id.spt @@ -55,12 +55,9 @@ if request.method == 'POST': if len(set(tip.amount.currency for tip in tips)) != 1: raise response.invalid_input(body.get('tips'), 'tips', 'body') - tips_weekly_sum = Money.sum((tip.amount for tip in tips), payin_currency) - amount_min = max( - constants.PAYIN_AMOUNTS['stripe']['min_acceptable'][payin_currency], - tips_weekly_sum - ) - amount_max = constants.PAYIN_AMOUNTS['stripe']['max_acceptable'][payin_currency] + prospect = PayinProspect(payer, tips, 'stripe') + amount_min = prospect.min_acceptable_amount + amount_max = prospect.max_acceptable_amount if payin_amount < amount_min or payin_amount > amount_max: raise response.error(400, _( "'{0}' is not an acceptable amount (min={1}, max={2})", @@ -170,7 +167,7 @@ if tippees: ] if len(set(tip.amount.currency for tip in tips)) != 1: raise response.invalid_input(tippees, 'beneficiary', 'querystring') - payment = PayinProspect(tips, 'stripe') + payment = PayinProspect(payer, tips, 'stripe') del tips elif not payin_id: diff --git a/www/%username/giving/schedule.spt b/www/%username/giving/schedule.spt index 03c35be263..c7a8580d58 100644 --- a/www/%username/giving/schedule.spt +++ b/www/%username/giving/schedule.spt @@ -38,7 +38,7 @@ if request.method == 'POST': else: new_amount = Money(new_amount, payin_currency, rounding=ROUND_HALF_UP) tips = payer.get_tips_to([tr['tippee_id'] for tr in sp.transfers]) - prospect = PayinProspect(tips, 'stripe') + prospect = PayinProspect(payer, tips, 'stripe') if payin_currency != prospect.currency: raise UnexpectedCurrency(new_amount, prospect.currency) amount_min = prospect.min_acceptable_amount @@ -77,7 +77,7 @@ if action: raise response.invalid_input(sp_id, 'id', 'querystring') if action == 'modify': sp.tips = payer.get_tips_to([tr['tippee_id'] for tr in sp.transfers]) - sp.prospect = PayinProspect(sp.tips, 'stripe') + sp.prospect = PayinProspect(payer, sp.tips, 'stripe') [---] text/html % extends "templates/layouts/settings.html"