diff --git a/r2/r2/controllers/promotecontroller.py b/r2/r2/controllers/promotecontroller.py index 6d483e51d..37411c4e3 100644 --- a/r2/r2/controllers/promotecontroller.py +++ b/r2/r2/controllers/promotecontroller.py @@ -977,18 +977,20 @@ class PromoteApiController(ApiController): rc = RenderableCampaign.from_campaigns(link, campaign) jquery.update_campaign(campaign._fullname, rc.render_html()) - @validatedForm(VSponsor('link'), - VModhash(), - link=VByName("link"), - campaign=VPromoCampaign("campaign"), - customer_id=VInt("customer_id", min=0), - pay_id=VInt("account", min=0), - edit=VBoolean("edit"), - address=ValidAddress( - ["firstName", "lastName", "company", "address", - "city", "state", "zip", "country", "phoneNumber"]), - creditcard=ValidCard(["cardNumber", "expirationDate", - "cardCode"])) + @validatedForm( + VSponsor('link'), + VModhash(), + link=VByName("link"), + campaign=VPromoCampaign("campaign"), + customer_id=VInt("customer_id", min=0), + pay_id=VInt("account", min=0), + edit=VBoolean("edit"), + address=ValidAddress( + ["firstName", "lastName", "company", "address", "city", "state", + "zip", "country", "phoneNumber"] + ), + creditcard=ValidCard(["cardNumber", "expirationDate", "cardCode"]), + ) def POST_update_pay(self, form, jquery, link, campaign, customer_id, pay_id, edit, address, creditcard): if not g.authorizenetapi: @@ -1033,7 +1035,9 @@ class PromoteApiController(ApiController): pay_id = edit_profile(c.user, address, creditcard, pay_id) - reason = None + if pay_id: + promote.new_payment_method(c.user, link) + if pay_id: success, reason = promote.auth_campaign(link, campaign, c.user, pay_id) @@ -1041,9 +1045,13 @@ class PromoteApiController(ApiController): if success: form.redirect(promote.promo_edit_url(link)) return - - msg = reason or _("failed to authenticate card. sorry.") - form.set_text(".status", msg) + else: + promote.failed_payment_method(c.user, link) + msg = reason or _("failed to authenticate card. sorry.") + form.set_text(".status", msg) + else: + promote.failed_payment_method(c.user, link) + form.set_text(".status", _("failed to authenticate card. sorry.")) @validate(VSponsor("link_name"), VModhash(), diff --git a/r2/r2/lib/emailer.py b/r2/r2/lib/emailer.py index 29e264e99..0689e4dde 100644 --- a/r2/r2/lib/emailer.py +++ b/r2/r2/lib/emailer.py @@ -257,6 +257,15 @@ def void_payment(thing, campaign, reason): reason=reason) +def suspicious_payment(user, link): + from r2.lib.pages import SuspiciousPaymentEmail + + email = "fraud@reddit.com" + body = SuspiciousPaymentEmail(user, link).render(style="email") + kind = Email.Kind.SUSPICIOUS_PAYMENT + _system_email(email, body, kind) + + def send_html_email(to_addr, from_addr, subject, html, subtype="html"): from r2.lib.filters import _force_utf8 msg = MIMEText(_force_utf8(html), subtype) diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index 38f2726ee..f9fc22f4e 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -2757,6 +2757,12 @@ class VerifyEmail(Templated): class Promo_Email(Templated): pass + +class SuspiciousPaymentEmail(Templated): + def __init__(self, user, link): + Templated.__init__(self, user=user, link=link) + + class ResetPassword(Templated): """Form for actually resetting a lost password, after the user has clicked on the link provided to them in the Password_Reset email diff --git a/r2/r2/lib/promote.py b/r2/r2/lib/promote.py index f627602a0..c73ddf03a 100644 --- a/r2/r2/lib/promote.py +++ b/r2/r2/lib/promote.py @@ -905,6 +905,29 @@ def get_spent_amount(campaign): return spent +PAYMENT_METHODS_ALERT_LIMIT = 3 +FAILED_PAYMENTS_ALERT_LIMIT = 5 + +def new_payment_method(user, link): + user._incr('num_payment_methods') + if (user.num_payment_methods > PAYMENT_METHODS_ALERT_LIMIT and + not user.payment_flagged): + payment_flag_user(user, link) + + +def failed_payment_method(user, link): + user._incr('num_failed_payments') + if (user.num_failed_payments > FAILED_PAYMENTS_ALERT_LIMIT and + not user.payment_flagged): + payment_flag_user(user, link) + + +def payment_flag_user(user, link): + user.payment_flagged = True + user._commit() + emailer.suspicious_payment(user, link) + + def Run(verbose=True): """reddit-job-update_promos: Intended to be run hourly to pull in scheduled changes to ads diff --git a/r2/r2/models/account.py b/r2/r2/models/account.py index 89ebdecb1..c8eef9c17 100644 --- a/r2/r2/models/account.py +++ b/r2/r2/models/account.py @@ -62,6 +62,8 @@ class Account(Thing): 'report_ignored', 'spammer', 'reported', 'gold_creddits', 'inbox_count', + 'num_payment_methods', + 'num_failed_payments', ) _int_prop_suffix = '_karma' _essentials = ('name', ) @@ -136,6 +138,7 @@ class Account(Thing): num_failed_payments=0, pref_show_snoovatar=False, gild_reveal_username=False, + payment_flagged=False, ) _preference_attrs = tuple(k for k in _defaults.keys() if k.startswith("pref_")) diff --git a/r2/r2/models/mail_queue.py b/r2/r2/models/mail_queue.py index c9f0f5eea..a3c65dbf4 100644 --- a/r2/r2/models/mail_queue.py +++ b/r2/r2/models/mail_queue.py @@ -306,6 +306,7 @@ class Email(object): "REFUNDED_PROMO", "VOID_PAYMENT", "GOLD_GIFT_CODE", + "SUSPICIOUS_PAYMENT", ) # Do not remove anything from this dictionary! See above comment. @@ -331,6 +332,7 @@ class Email(object): Kind.REFUNDED_PROMO: _("[reddit] your campaign didn't get enough impressions"), Kind.VOID_PAYMENT: _("[reddit] your payment has been voided"), Kind.GOLD_GIFT_CODE: _("[reddit] your reddit gold gift code"), + Kind.SUSPICIOUS_PAYMENT: _("[selfserve] suspicious payment alert"), } def __init__(self, user, thing, email, from_name, date, ip, diff --git a/r2/r2/templates/suspiciouspaymentemail.email b/r2/r2/templates/suspiciouspaymentemail.email new file mode 100644 index 000000000..b990e155c --- /dev/null +++ b/r2/r2/templates/suspiciouspaymentemail.email @@ -0,0 +1,31 @@ +## The contents of this file are subject to the Common Public Attribution +## License Version 1.0. (the "License"); you may not use this file except in +## compliance with the License. You may obtain a copy of the License at +## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public +## License Version 1.1, but Sections 14 and 15 have been added to cover use of +## software over a computer network and provide for limited attribution for the +## Original Developer. In addition, Exhibit A has been modified to be +## consistent with Exhibit B. +## +## Software distributed under the License is distributed on an "AS IS" basis, +## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for +## the specific language governing rights and limitations under the License. +## +## The Original Code is reddit. +## +## The Original Developer is the Initial Developer. The Initial Developer of +## the Original Code is reddit Inc. +## +## All portions of the code written by reddit are Copyright (c) 2006-2014 +## reddit Inc. All Rights Reserved. +############################################################################### + +<%! + from r2.lib.promote import promo_edit_url +%> + +<%namespace file="utils.html" import="plain_link"/> + +There is suspicious payment activity by ${plain_link("/u/%s" % thing.user.name, "/user/%s/promoted" % thing.user.name, _sr_path=False)}. + +This report was triggered by the promotion ${plain_link(thing.link.title, promo_edit_url(thing.link), _sr_path=False)}.