diff --git a/r2/r2/controllers/ipn.py b/r2/r2/controllers/ipn.py index 5026534cf..14b7f41bd 100644 --- a/r2/r2/controllers/ipn.py +++ b/r2/r2/controllers/ipn.py @@ -29,6 +29,7 @@ import base64 from BeautifulSoup import BeautifulStoneSoup from pylons import c, g, request from pylons.i18n import _ +from sqlalchemy.exc import IntegrityError from r2.controllers.reddit_base import RedditController from r2.lib.filters import _force_unicode @@ -50,12 +51,14 @@ from r2.models import ( accountid_from_paypalsubscription, admintools, cancel_subscription, + Comment, create_claimed_gold, create_gift_gold, make_comment_gold_message, NotFound, send_system_message, Thing, + update_gold_transaction, ) @@ -564,3 +567,152 @@ class IpnController(RedditController): payment_blob["status"] = "processed" g.hardcache.set(blob_key, payment_blob, 86400 * 30) + + +class GoldException(Exception): pass + + +def validate_blob(custom): + """Validate payment_blob and return a dict with everything looked up.""" + ret = {} + + if not custom: + raise GoldException('no custom') + + payment_blob = g.hardcache.get('payment_blob-%s' % str(custom)) + if not payment_blob: + raise GoldException('no payment_blob') + + if not ('account_id' in payment_blob and + 'account_name' in payment_blob): + raise GoldException('no account_id') + + try: + buyer = Account._byID(payment_blob['account_id'], data=True) + ret['buyer'] = buyer + except NotFound: + raise GoldException('bad account_id') + + if not buyer.name.lower() == payment_blob['account_name'].lower(): + raise GoldException('buyer mismatch') + + goldtype = payment_blob['goldtype'] + ret['goldtype'] = goldtype + + if goldtype == 'gift': + recipient_name = payment_blob.get('recipient', None) + if not recipient_name: + raise GoldException('gift missing recpient') + try: + recipient = Account._by_name(recipient_name) + ret['recipient'] = recipient + except NotFound: + raise GoldException('bad recipient') + comment_fullname = payment_blob.get('comment', None) + if comment_fullname: + try: + ret['comment'] = Comment._by_fullname(comment_fullname) + except NotFound: + raise GoldException('bad comment') + ret['signed'] = payment_blob.get('signed', False) + ret['giftmessage'] = payment_blob.get('giftmessage', False) + elif goldtype not in ('onetime', 'autorenew', 'creddits'): + raise GoldException('bad goldtype') + + return ret + + +def gold_lock(user): + return g.make_lock('gold_purchase', 'gold_%s' % user._id) + + +def complete_gold_purchase(secret, transaction_id, payer_email, payer_id, + subscription_id, pennies, goldtype, buyer, recipient, + signed, giftmessage, comment): + """After receiving a message from a payment processor, apply gold. + + Shared endpoint for all payment processing systems. Validation of gold + purchase (sender, recipient, etc.) should happen before hitting this. + + """ + + gold_recipient = recipient or buyer + with gold_lock(gold_recipient): + gold_recipient._sync_latest() + months, days = months_and_days_from_pennies(pennies) + + if goldtype in ('onetime', 'autorenew'): + admintools.engolden(buyer, days) + if goldtype == 'onetime': + subject = "thanks for buying reddit gold!" + if g.lounge_reddit: + lounge_url = "/r/" + g.lounge_reddit + message = strings.lounge_msg % dict(link=lounge_url) + else: + message = ":)" + send_system_message(buyer, subject, message) + else: + subject = "your reddit gold has been renewed!" + message = ("see the details of your subscription on " + "[your userpage](/u/%s)" % buyer.name) + send_system_message(buyer, subject, message) + + elif goldtype == 'creddits': + buyer._incr('gold_creddits', months) + subject = "thanks for buying creddits!" + message = ("To spend them, visit http://%s/gold or your favorite " + "person's userpage." % (g.domain)) + send_system_message(buyer, subject, message) + + elif goldtype == 'gift': + send_gift(buyer, recipient, months, days, signed, giftmessage, + comment) + subject = "thanks for giving reddit gold!" + message = "Your gift to %s has been delivered." % recipient.name + send_system_message(buyer, subject, message) + + status = 'processed' + secret_pieces = [goldtype] + if goldtype == 'gift': + secret_pieces.append(recipient.name) + secret_pieces.append(secret) + secret = '-'.join(secret_pieces) + + try: + create_claimed_gold(transaction_id, payer_email, payer_id, pennies, + days, secret_pieces, buyer._id, c.start_time, + subscr_id=subscription_id, status=status) + except IntegrityError: + g.log.error('gold: got duplicate gold transaction') + + +def subtract_gold_days(user, days): + user.gold_expiration -= timedelta(days=days) + if user.gold_expiration < datetime.now(g.display_tz): + user.gold = False + user._commit() + + +def subtract_gold_creddits(user, num): + user._incr('gold_creddits', -num) + + +def reverse_gold_purchase(transaction_id, goldtype, buyer, pennies, + recipient=None): + gold_recipient = recipient or buyer + with gold_lock(gold_recipient): + gold_recipient._sync_latest() + months, days = months_and_days_from_pennies(pennies) + + if goldtype in ('onetime', 'autorenew'): + subtract_gold_days(buyer, days) + + elif goldtype == 'creddits': + subtract_gold_creddits(buyer, months) + + elif goldtype == 'gift': + subtract_gold_days(recipient, days) + subject = 'your gifted gold has been reversed' + message = 'sorry, but the payment was reversed' + send_system_message(recipient, subject, message) + update_gold_transaction(transaction_id, 'reversed') diff --git a/r2/r2/models/gold.py b/r2/r2/models/gold.py index 405b1be7d..a7a551c9b 100644 --- a/r2/r2/models/gold.py +++ b/r2/r2/models/gold.py @@ -362,3 +362,15 @@ def process_uncharged(): if trans_id.startswith('g'): trans_id = trans_id[1:] process_google_transaction(trans_id) + + +def retrieve_gold_transaction(transaction_id): + s = sa.select([gold_table], gold_table.c.trans_id == transaction_id) + res = s.execute().fetchall() + if res: + return res[0] # single row per transaction_id + + +def update_gold_transaction(transaction_id, status): + rp = gold_table.update(gold_table.c.trans_id == str(transaction_id), + values={gold_table.c.status: status}).execute()