from regluit.core.models import Campaign, Wishlist from regluit.payment.models import Transaction, Receiver, PaymentResponse from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.conf import settings from regluit.payment.parameters import * from regluit.payment.paypal import IPN_SENDER_STATUS_COMPLETED from regluit.payment.signals import transaction_charged, pledge_modified, pledge_created if settings.PAYMENT_PROCESSOR == 'paypal': from regluit.payment.paypal import Pay, Finish, Preapproval, ProcessIPN, CancelPreapproval, PaymentDetails, PreapprovalDetails, RefundPayment from regluit.payment.paypal import Pay as Execute elif settings.PAYMENT_PROCESSOR == 'amazon': from regluit.payment.amazon import Pay, Execute, Finish, Preapproval, ProcessIPN, CancelPreapproval, PaymentDetails, PreapprovalDetails, RefundPayment import uuid import traceback from regluit.utils.localdatetime import now from dateutil.relativedelta import relativedelta import logging from decimal import Decimal as D from xml.dom import minidom import urllib, urlparse from django.conf import settings logger = logging.getLogger(__name__) def append_element(doc, parent, name, text): element = doc.createElement(name) parent.appendChild(element) text_node = doc.createTextNode(text) element.appendChild(text_node) return element # at this point, there is no internal context and therefore, the methods of PaymentManager can be recast into static methods class PaymentManager( object ): def __init__( self, embedded=False): self.embedded = embedded def processIPN(self, request, module): # Forward to our payment processor mod = __import__("regluit.payment." + module, fromlist=[str(module)]) method = getattr(mod, "ProcessIPN") return method(request) def update_preapproval(self, transaction): """Update a transaction to hold the data from a PreapprovalDetails on that transaction""" t = transaction method = getattr(transaction.get_payment_class(), "PreapprovalDetails") p = method(t) preapproval_status = {'id':t.id, 'key':t.preapproval_key} if p.error() or not p.success(): logger.info("Error retrieving preapproval details for transaction %d" % t.id) preapproval_status["error"] = "An error occurred while verifying this transaction, see server logs for details" else: # Check the transaction status if t.status != p.status: preapproval_status["status"] = {'ours':t.status, 'theirs':p.status} t.status = p.status t.local_status = p.local_status t.save() # check the currency code if t.currency != p.currency: preapproval_status["currency"] = {'ours':t.currency, 'theirs':p.currency} t.currency = p.currency t.save() # Check the amount if t.max_amount != D(p.amount): preapproval_status["amount"] = {'ours':t.max_amount, 'theirs':p.amount} t.max_amount = p.amount t.save() # Check approved if t.approved != p.approved: preapproval_status["approved"] = {'ours':t.approved, 'theirs':p.approved} t.approved = p.approved t.save() # In amazon FPS, we may not have a pay_key via the return URL, update here try: if t.pay_key != p.pay_key: preapproval_status['pay_key'] = {'ours':t.pay_key, 'theirs':p.pay_key} t.pay_key = p.pay_key t.save() except: # No problem, p.pay_key is not defined for paypal function blah = "blah" return preapproval_status def update_payment(self, transaction): """Update a transaction to hold the data from a PaymentDetails on that transaction""" t = transaction payment_status = {'id':t.id} method = getattr(transaction.get_payment_class(), "PaymentDetails") p = method(t) if p.error() or not p.success(): logger.info("Error retrieving payment details for transaction %d" % t.id) payment_status['error'] = "An error occurred while verifying this transaction, see server logs for details" else: # Check the transaction status if t.status != p.status: payment_status['status'] = {'ours': t.status, 'theirs': p.status} t.status = p.status t.local_status = p.local_status t.save() receivers_status = [] for r in p.transactions: # This is only supported for paypal at this time try: receiver = Receiver.objects.get(transaction=t, email=r['email']) receiver_status = {'email':r['email']} logger.info(r) logger.info(receiver) # Check for updates on each receiver's status. Note that unprocessed delayed chained payments # will not have a status code or txn id code if receiver.status != r['status']: receiver_status['status'] = {'ours': receiver.status, 'theirs': r['status']} receiver.status = r['status'] receiver.save() if receiver.txn_id != r['txn_id']: receiver_status['txn_id'] = {'ours':receiver.txn_id, 'theirs':r['txn_id']} receiver.txn_id = r['txn_id'] receiver.save() except: traceback.print_exc() if not set(["status","txn_id"]).isdisjoint(receiver_status.keys()): receivers_status.append(receiver_status) if len(receivers_status): payment_status["receivers"] = receivers_status return payment_status def checkStatus(self, past_days=None, transactions=None): ''' Run through all pay transactions and verify that their current status is as we think. Allow for a list of transactions to be passed in or for the method to check on all transactions within the given past_days ''' DEFAULT_DAYS_TO_CHECK = 3 status = {'payments':[], 'preapprovals':[]} # look at all PAY transactions for stated number of past days; if past_days is not int, get all Transaction # only PAY transactions have date_payment not None if transactions is None: if past_days is None: past_days = DEFAULT_DAYS_TO_CHECK try: ref_date = now() - relativedelta(days=int(past_days)) payment_transactions = Transaction.objects.filter(date_payment__gte=ref_date) except: ref_date = now() payment_transactions = Transaction.objects.filter(date_payment__isnull=False) logger.info(payment_transactions) # Now look for preapprovals that have not been paid and check on their status preapproval_transactions = Transaction.objects.filter(date_authorized__gte=ref_date, date_payment=None, type=PAYMENT_TYPE_AUTHORIZATION) logger.info(preapproval_transactions) transactions = payment_transactions | preapproval_transactions for t in transactions: # deal with preapprovals if t.date_payment is None: preapproval_status = self.update_preapproval(t) logger.info("transaction: {0}, preapproval_status: {1}".format(t, preapproval_status)) if not set(['status', 'currency', 'amount', 'approved']).isdisjoint(set(preapproval_status.keys())): status["preapprovals"].append(preapproval_status) # update payments else: payment_status = self.update_payment(t) if not set(["status", "receivers"]).isdisjoint(payment_status.keys()): status["payments"].append(payment_status) # Clear out older, duplicate preapproval transactions cleared_list = [] for p in transactions: # pick out only the preapprovals if p.date_payment is None and p.type == PAYMENT_TYPE_AUTHORIZATION and p.status == TRANSACTION_STATUS_ACTIVE and p not in cleared_list: # keep only the newest transaction for this user and campaign user_transactions_for_campaign = Transaction.objects.filter(user=p.user, status=TRANSACTION_STATUS_ACTIVE, campaign=p.campaign).order_by('-date_authorized') if len(user_transactions_for_campaign) > 1: logger.info("Found %d active transactions for campaign" % len(user_transactions_for_campaign)) self.cancel_related_transaction(user_transactions_for_campaign[0], status=TRANSACTION_STATUS_ACTIVE, campaign=transactions[0].campaign) cleared_list.extend(user_transactions_for_campaign) # Note, we may need to call checkstatus again here return status def run_query(self, transaction_list, summary=True, campaign_total=False, pledged=False, authorized=False, incomplete=False, completed=False, pending=False, error=False, failed=False, **kwargs): ''' Generic query handler for returning summary and transaction info, see query_user and query_campaign campaign_total=True includes all payment types which should count towards campaign total ''' if campaign_total: # must double check when adding Paypal or other # return only ACTIVE transactions with approved=True list = transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION, approved=True).exclude(status=TRANSACTION_STATUS_CANCELED) list = list | transaction_list.filter(type=PAYMENT_TYPE_INSTANT, status=TRANSACTION_STATUS_COMPLETE) else: list = Transaction.objects.none() if pledged: list = list | transaction_list.filter(type=PAYMENT_TYPE_INSTANT, status=TRANSACTION_STATUS_COMPLETE) if authorized: # return only ACTIVE transactions with approved=True list = list | transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION, status=TRANSACTION_STATUS_ACTIVE, approved=True) if incomplete: list = list | transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION, status=TRANSACTION_STATUS_INCOMPLETE) if completed: list = list | transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION, status=TRANSACTION_STATUS_COMPLETE) if pending: list = list | transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION, status=TRANSACTION_STATUS_PENDING) if error: list = list | transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION, status=TRANSACTION_STATUS_ERROR) if failed: list = list | transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION, status=TRANSACTION_STATUS_FAILED) if summary: amount = D('0.00') for t in list: if t.type==PAYMENT_TYPE_INSTANT: for r in t.receiver_set.all(): # # Currently receivers are only used for paypal, so keep the paypal status code here # if r.status == IPN_SENDER_STATUS_COMPLETED: # or IPN_SENDER_STATUS_COMPLETED # individual senders may not have been paid due to errors, and disputes/chargebacks only appear here amount += r.amount else: amount += t.amount return amount else: return list def query_user(self, user, **kwargs): ''' query_user Returns either an amount or list of transactions for a user summary: if true, return a float of the total, if false, return a list of transactions return value: either a float summary or a list of transactions Note: this method appears to be unused. ''' transactions = Transaction.objects.filter(user=user) return self.run_query(transactions, **kwargs) def query_campaign(self, campaign, **kwargs ): ''' query_campaign Returns either an amount or list of transactions for a campaign summary: if true, return a float of the total, if false, return a list of transactions return value: either a float summary or a list of transactions ''' transactions = Transaction.objects.filter(campaign=campaign) return self.run_query(transactions, **kwargs) def execute_campaign(self, campaign): ''' execute_campaign attempts to execute all pending transactions for a campaign. return value: returns a list of transactions with the status of each receiver/transaction updated ''' # only allow active transactions to go through again, if there is an error, intervention is needed transactions = Transaction.objects.filter(campaign=campaign, status=TRANSACTION_STATUS_ACTIVE) for t in transactions: # # Currently receivers are only used for paypal, so it is OK to leave the paypal info here # receiver_list = [{'email':settings.PAYPAL_GLUEJAR_EMAIL, 'amount':t.amount}, {'email':campaign.paypal_receiver, 'amount':D(t.amount) * (D('1.00') - D(str(settings.GLUEJAR_COMMISSION)))}] self.execute_transaction(t, receiver_list) # TO DO: update campaign status # Should this be done first before executing the transactions? # How does the success/failure of transactions affect states of campaigns return transactions def finish_campaign(self, campaign): ''' finish_campaign attempts to execute all remaining payment to non-primary receivers This is currently only supported for paypal return value: returns a list of transactions with the status of each receiver/transaction updated ''' # QUESTION: to figure out which transactions are in a state in which the payment to the primary recipient is done but not to secondary recipient # Consider two possibilities: status=IPN_PAY_STATUS_INCOMPLETE or execution = EXECUTE_TYPE_CHAINED_DELAYED # which one? Let's try the second one # only allow incomplete transactions to go through again, if there is an error, intervention is needed transactions = Transaction.objects.filter(campaign=campaign, execution=EXECUTE_TYPE_CHAINED_DELAYED) for t in transactions: result = self.finish_transaction(t) # TO DO: update campaign status return transactions def cancel_campaign(self, campaign): ''' cancel_campaign attempts to cancel active preapprovals related to the campaign return value: returns a list of transactions with the status of each receiver/transaction updated ''' transactions = Transaction.objects.filter(campaign=campaign, status=TRANSACTION_STATUS_ACTIVE) for t in transactions: result = self.cancel_transaction(t) # TO DO: update campaign status return transactions def finish_transaction(self, transaction): ''' finish_transaction calls the paypal API to execute payment to non-primary receivers transaction: the transaction we want to complete ''' if transaction.execution != EXECUTE_TYPE_CHAINED_DELAYED: logger.error("FinishTransaction called with invalid execution type") return False # mark this transaction as executed transaction.date_executed = now() transaction.save() method = getattr(transaction.get_payment_class(), "Finish") p = method(transaction) # Create a response for this envelope = p.envelope() if envelope: correlation = p.correlation_id() timestamp = p.timestamp() r = PaymentResponse.objects.create(api=p.url, correlation_id = correlation, timestamp = timestamp, info = p.raw_response, transaction=transaction) if p.success() and not p.error(): logger.info("finish_transaction Success") return True else: transaction.error = p.error_string() transaction.save() logger.info("finish_transaction error " + p.error_string()) return False def execute_transaction(self, transaction, receiver_list): ''' execute_transaction executes a single pending transaction. transaction: the transaction object to execute receiver_list: a list of receivers for the transaction, in this format: [ {'email':'email-1', 'amount':amount1}, {'email':'email-2', 'amount':amount2} ] return value: a bool indicating the success or failure of the process. Please check the transaction status after the IPN has completed for full information ''' if len(transaction.receiver_set.all()) > 0: # we are re-submitting a transaction, wipe the old receiver list transaction.receiver_set.all().delete() transaction.create_receivers(receiver_list) # Mark as payment attempted so we will poll this periodically for status changes transaction.date_payment = now() transaction.save() method = getattr(transaction.get_payment_class(), "Execute") p = method(transaction) # Create a response for this envelope = p.envelope() if envelope: correlation = p.correlation_id() timestamp = p.timestamp() r = PaymentResponse.objects.create(api=p.api(), correlation_id = correlation, timestamp = timestamp, info = p.raw_response, transaction=transaction) # We will update our transaction status when we receive the IPN if p.success() and not p.error(): transaction.pay_key = p.key() transaction.save() logger.info("execute_transaction Success") transaction_charged.send(sender=self, transaction=transaction) return True else: transaction.error = p.error_string() transaction.save() logger.info("execute_transaction Error: " + p.error_string()) return False def cancel_transaction(self, transaction): ''' cancel cancels a pre-approved transaction return value: True if successful, false otherwise ''' method = getattr(transaction.get_payment_class(), "CancelPreapproval") p = method(transaction) # Create a response for this envelope = p.envelope() if envelope: correlation = p.correlation_id() timestamp = p.timestamp() r = PaymentResponse.objects.create(api=p.url, correlation_id = correlation, timestamp = timestamp, info = p.raw_response, transaction=transaction) if p.success() and not p.error(): logger.info("Cancel Transaction " + str(transaction.id) + " Completed") return True else: transaction.error = p.error_string() transaction.save() logger.info("Cancel Transaction " + str(transaction.id) + " Failed with error: " + p.error_string()) return False def authorize(self, currency, target, amount, expiry=None, campaign=None, list=None, user=None, return_url=None, nevermind_url=None, anonymous=False, premium=None, paymentReason="unglue.it Pledge", modification=False): ''' authorize authorizes a set amount of money to be collected at a later date currency: a 3-letter paypal currency code, i.e. USD target: a defined target type, i.e. TARGET_TYPE_CAMPAIGN, TARGET_TYPE_LIST, TARGET_TYPE_NONE amount: the amount to authorize campaign: optional campaign object(to be set with TARGET_TYPE_CAMPAIGN) list: optional list object(to be set with TARGET_TYPE_LIST) user: optional user object return_url: url to redirect supporter to after a successful PayPal transaction nevermind_url: url to send supporter to if support hits cancel while in middle of PayPal transaction anonymous: whether this pledge is anonymous premium: the premium selected by the supporter for this transaction paymentReason: a memo line that will show up in the Payer's Amazon (and Paypal?) account modification: whether this authorize call is part of a modification of an existing pledge return value: a tuple of the new transaction object and a re-direct url. If the process fails, the redirect url will be None ''' t = Transaction.objects.create(amount=amount, max_amount=amount, type=PAYMENT_TYPE_AUTHORIZATION, execution = EXECUTE_TYPE_CHAINED_DELAYED, target=target, currency=currency, status='NONE', campaign=campaign, list=list, user=user, anonymous=anonymous, premium=premium ) # we might want to not allow for a return_url or nevermind_url to be passed in but calculated # here because we have immediate access to the Transaction object. if nevermind_url is None: nevermind_path = "{0}?{1}".format(reverse('pledge_nevermind'), urllib.urlencode({'tid':t.id})) nevermind_url = urlparse.urljoin(settings.BASE_URL, nevermind_path) if return_url is None: return_path = "{0}?{1}".format(reverse('pledge_complete'), urllib.urlencode({'tid':t.id})) return_url = urlparse.urljoin(settings.BASE_URL, return_path) method = getattr(t.get_payment_class(), "Preapproval") p = method(t, amount, expiry, return_url=return_url, nevermind_url=nevermind_url, paymentReason=paymentReason) # Create a response for this envelope = p.envelope() if envelope: r = PaymentResponse.objects.create(api=p.url, correlation_id = p.correlation_id(), timestamp = p.timestamp(), info = p.raw_response, transaction=t) if p.success() and not p.error(): t.preapproval_key = p.key() t.save() url = p.next_url() logger.info("Authorize Success: " + url) # modification and initial pledge use different notification templates -- # decide which to send # we need to fire notifications at the first point at which we are sure # that the transaction has successfully completed; triggering notifications # when the transaction is initiated risks sending notifications on transactions # that for whatever reason fail. will need other housekeeping to handle those. # sadly this point is not yet late enough in the process -- needs to be moved # until after we are certain. if not modification: # BUGBUG: # send the notice here for now # this is actually premature since we're only about to send the user off to the payment system to # authorize a charge pledge_created.send(sender=self, transaction=t) return t, url else: t.error = p.error_string() t.save() logger.info("Authorize Error: " + p.error_string()) return t, None def cancel_related_transaction(self, transaction, status=TRANSACTION_STATUS_ACTIVE, campaign=None): ''' Cancels any other similar status transactions for the same campaign. Used with modify code Returns the number of transactions successfully canceled ''' related_transactions = Transaction.objects.filter(status=status, user=transaction.user) if len(related_transactions) == 0: return 0 if campaign: related_transactions = related_transactions.filter(campaign=campaign) canceled = 0 for t in related_transactions: if t.id == transaction.id: # keep our transaction continue if self.cancel_transaction(t): canceled = canceled + 1 # send notice about modification of transaction if transaction.amount > t.amount: # this should be the only one that happens up_or_down = "increased" elif transaction.amount < t.amount: # we shouldn't expect any case in which this happens up_or_down = "decreased" else: # we shouldn't expect any case in which this happens up_or_down = None pledge_modified.send(sender=self, transaction=transaction, up_or_down=up_or_down) else: logger.error("Failed to cancel transaction {0} for related transaction {1} ".format(t, transaction)) return canceled def modify_transaction(self, transaction, amount, expiry=None, anonymous=None, premium=None, return_url=None, nevermind_url=None, paymentReason=None): ''' modify Modifies a transaction. The only type of modification allowed is to the amount and expiration date amount: the new amount expiry: the new expiration date, or if none the current expiration date will be used anonymous: new anonymous value; if None, then keep old value premium: new premium selected; if None, then keep old value return_url: the return URL after the preapproval(if needed) nevermind_url: the cancel url after the preapproval(if needed) paymentReason: a memo line that will show up in the Payer's Amazon (and Paypal?) account return value: True if successful, False otherwise. An optional second parameter for the forward URL if a new authorhization is needed ''' # Can only modify the amount of a preapproval for now if transaction.type != PAYMENT_TYPE_AUTHORIZATION: logger.info("Error, attempt to modify an invalid transaction type") return False, None # Can only modify an active, pending transaction. If it is completed, we need to do a refund. If it is incomplete, # then an IPN may be pending and we cannot touch it if transaction.status != TRANSACTION_STATUS_ACTIVE: logger.info("Error, attempt to modify a transaction that is not active") return False, None # if any of expiry, anonymous, or premium is None, use the existing value if expiry is None: expiry = transaction.date_expired if anonymous is None: anonymous = transaction.anonymous if premium is None: premium = transaction.premium if amount > transaction.max_amount or expiry != transaction.date_expired: # Start a new authorization for the new amount t, url = self.authorize(transaction.currency, transaction.target, amount, expiry, transaction.campaign, transaction.list, transaction.user, return_url, nevermind_url, transaction.anonymous, premium, paymentReason, True) if t and url: # Need to re-direct to approve the transaction logger.info("New authorization needed, redirection to url %s" % url) # Do not cancel the transaction here, wait until we get confirmation that the transaction is complete # then cancel all other active transactions for this campaign #self.cancel_transaction(transaction) # while it would seem to make sense to send a pledge notification change here # if we do, we will also send notifications when we initiate but do not # successfully complete a pledge modification return True, url else: # a problem in authorize logger.info("Error, unable to start a new authorization") # should we send a pledge_modified signal with state="failed" and a # corresponding notification to the user? that would go here. return False, None elif amount <= transaction.max_amount: # Update transaction but leave the preapproval alone transaction.amount = amount transaction.anonymous = anonymous transaction.premium = premium transaction.save() logger.info("Updated amount of transaction to %f" % amount) # since modifying pledges downwards happens immediately and only within our # db, we don't have to wait until we hear back from amazon to be assured of # success; send the notification immediately pledge_modified.send(sender=self, transaction=transaction, up_or_down="decreased") return True, None else: # this shouldn't happen return False, None def refund_transaction(self, transaction): ''' refund Refunds a transaction. The money for the transaction may have gone to a number of places. We can only refund money that is in our account return value: True if successful, false otherwise ''' # First check if a payment has been made. It is possible that some of the receivers may be incomplete # We need to verify that the refund API will cancel these if transaction.status != TRANSACTION_STATUS_COMPLETE: logger.info("Refund Transaction failed, invalid transaction status") return False method = getattr(transaction.get_payment_class(), "RefundPayment") p = method(transaction) # Create a response for this envelope = p.envelope() if envelope: correlation = p.correlation_id() timestamp = p.timestamp() r = PaymentResponse.objects.create(api=p.url, correlation_id = correlation, timestamp = timestamp, info = p.raw_response, transaction=transaction) if p.success() and not p.error(): logger.info("Refund Transaction " + str(transaction.id) + " Completed") return True else: transaction.error = p.error_string() transaction.save() logger.info("Refund Transaction " + str(transaction.id) + " Failed with error: " + p.error_string()) return False def pledge(self, currency, target, receiver_list, campaign=None, list=None, user=None, return_url=None, nevermind_url=None, anonymous=False, premium=None): ''' pledge Performs an instant payment currency: a 3-letter paypal currency code, i.e. USD target: a defined target type, i.e. TARGET_TYPE_CAMPAIGN, TARGET_TYPE_LIST, TARGET_TYPE_NONE receiver_list: a list of receivers for the transaction, in this format: [ {'email':'email-1', 'amount':amount1}, {'email':'email-2', 'amount':amount2} ] campaign: optional campaign object(to be set with TARGET_TYPE_CAMPAIGN) list: optional list object(to be set with TARGET_TYPE_LIST) user: optional user object return_url: url to redirect supporter to after a successful PayPal transaction nevermind_url: url to send supporter to if support hits cancel while in middle of PayPal transaction anonymous: whether this pledge is anonymous premium: the premium selected by the supporter for this transaction return value: a tuple of the new transaction object and a re-direct url. If the process fails, the redirect url will be None ''' amount = D('0.00') # for chained payments, first amount is the total amount amount = D(receiver_list[0]['amount']) t = Transaction.objects.create(amount=amount, max_amount=amount, type=PAYMENT_TYPE_INSTANT, execution=EXECUTE_TYPE_CHAINED_INSTANT, target=target, currency=currency, status='NONE', campaign=campaign, list=list, user=user, date_payment=now(), anonymous=anonymous, premium=premium ) t.create_receivers(receiver_list) method = getattr(t.get_payment_class(), "Pay") p = method(t,return_url=return_url, nevermind_url=nevermind_url) # Create a response for this envelope = p.envelope() logger.info(envelope) if envelope: r = PaymentResponse.objects.create(api=p.api(), correlation_id = p.correlation_id(), timestamp = p.timestamp(), info = p.raw_response, transaction=t) if p.success() and not p.error(): t.pay_key = p.key() t.status = TRANSACTION_STATUS_CREATED t.save() if self.embedded: url = p.embedded_url() logger.info(url) else: url = p.next_url() logger.info("Pledge Success: " + url) return t, url else: t.error = p.error_string() t.save() logger.info("Pledge Error: %s" % p.error_string()) return t, None