diff --git a/payment/manager.py b/payment/manager.py index e008f5c1..51ab5b86 100644 --- a/payment/manager.py +++ b/payment/manager.py @@ -4,6 +4,7 @@ from django.contrib.auth.models import User from regluit.payment.parameters import * from regluit.payment.paypal import Pay, Execute, IPN, IPN_TYPE_PAYMENT, IPN_TYPE_PREAPPROVAL, IPN_TYPE_ADJUSTMENT, IPN_PAY_STATUS_ACTIVE, IPN_PAY_STATUS_INCOMPLETE from regluit.payment.paypal import Preapproval, IPN_PAY_STATUS_COMPLETED, CancelPreapproval, PaymentDetails, PreapprovalDetails, IPN_SENDER_STATUS_COMPLETED, IPN_TXN_STATUS_COMPLETED +from regluit.payment.paypal import RefundPayment import uuid import traceback from datetime import datetime @@ -220,7 +221,7 @@ class PaymentManager( object ): elif ipn.transaction_type == IPN_TYPE_ADJUSTMENT: # a chargeback, reversal or refund for an existng payment - + uniqueID = ipn.uniqueID() if uniqueID: t = Transaction.objects.get(secret=uniqueID) @@ -233,6 +234,23 @@ class PaymentManager( object ): # Reason code indicates more details of the adjustment type t.reason = ipn.reason_code + + # Update the receiver status codes + for item in ipn.transactions: + + try: + r = Receiver.objects.get(transaction=t, email=item['receiver']) + logger.info(item) + # one of the IPN_SENDER_STATUS codes defined in paypal.py, If we are doing delayed chained + # payments, then there is no status or id for non-primary receivers. Leave their status alone + r.status = item['status_for_sender_txn'] + r.save() + except: + # Log an exception if we have a receiver that is not found. This will be hit + # for delayed chained payments as there is no status or id for the non-primary receivers yet + traceback.print_exc() + + t.save() elif ipn.transaction_type == IPN_TYPE_PREAPPROVAL: @@ -590,6 +608,7 @@ class PaymentManager( object ): ''' t = Transaction.objects.create(amount=amount, + max_amount=amount, type=PAYMENT_TYPE_AUTHORIZATION, execution = EXECUTE_TYPE_CHAINED_DELAYED, target=target, @@ -629,6 +648,121 @@ class PaymentManager( object ): logger.info("Authorize Error: " + p.error_string()) return t, None + def modify_transaction(self, transaction, amount=None, expiry=None, return_url=None, cancel_url=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 + return_url: the return URL after the preapproval(if needed) + cancel_url: the cancel url after the preapproval(if needed) + + return value: True if successful, false otherwise. An optional second parameter for the forward URL if a new authorhization is needed + ''' + + if not amount: + logger.info("Error, no amount speicified") + return False + + if transaction.type != PAYMENT_TYPE_AUTHORIZATION: + # Can only modify the amount of a preapproval for now + logger.info("Error, attempt to modify an invalid transaction type") + return False, None + + if transaction.status != IPN_PAY_STATUS_ACTIVE: + # 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 + logger.info("Error, attempt to modify a transaction that is not active") + return False, None + + if not expiry: + # Use the old expiration date + expiry = transaction.date_expired + + if amount > transaction.max_amount or expiry != transaction.date_expired: + + # Increase or expiuration change, cancel and start again + self.cancel_transaction(transaction) + + # 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, + cancel_url, + transaction.anonymous) + + if t and url: + # Need to re-direct to approve the transaction + logger.info("New authorization needed, redirectiont to url %s" % url) + return True, url + else: + # No amount change necessary + logger.info("Error, unable to start a new authorization") + return False, None + + elif amount < transaction.max_amount: + # Change the amount but leave the preapproval alone + transaction.amount = amount + transaction.save() + logger.info("Updated amount of transaction to %f" % amount) + return True, None + + else: + # No changes + logger.info("Error, no modifications requested") + 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 != IPN_PAY_STATUS_COMPLETED: + logger.info("Refund Transaction failed, invalid transaction status") + return False + + p = RefundPayment(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, cancel_url=None, anonymous=False): ''' pledge @@ -658,7 +792,8 @@ class PaymentManager( object ): # for chained payments, first amount is the total amount amount = D(receiver_list[0]['amount']) - t = Transaction.objects.create(amount=amount, + t = Transaction.objects.create(amount=amount, + max_amount=amount, type=PAYMENT_TYPE_INSTANT, execution=EXECUTE_TYPE_CHAINED_INSTANT, target=target, diff --git a/payment/models.py b/payment/models.py index d680b78a..e0f9151b 100644 --- a/payment/models.py +++ b/payment/models.py @@ -21,6 +21,7 @@ class Transaction(models.Model): # amount & currency -- amount of money and its currency involved for transaction amount = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99 + max_amount = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99 currency = models.CharField(max_length=10, default='USD', null=True) # a unique ID that can be passed to PayPal to track a transaction diff --git a/payment/paypal.py b/payment/paypal.py index b783ad4c..1b1bfd5d 100644 --- a/payment/paypal.py +++ b/payment/paypal.py @@ -155,7 +155,8 @@ class PaypalEnvelopeRequest: url = None def ack( self ): - if self.response.has_key( 'responseEnvelope' ) and self.response['responseEnvelope'].has_key( 'ack' ): + + if self.response and self.response.has_key( 'responseEnvelope' ) and self.response['responseEnvelope'].has_key( 'ack' ): return self.response['responseEnvelope']['ack'] else: return None @@ -177,17 +178,22 @@ class PaypalEnvelopeRequest: return False def error_data(self): - if self.response.has_key('error'): + + if self.response and self.response.has_key('error'): return self.response['error'] else: return None def error_id(self): - if self.response.has_key('error'): + + if self.response and self.response.has_key('error'): return self.response['error'][0]['errorId'] + else: + return None def error_string(self): - if self.response.has_key('error'): + + if self.response and self.response.has_key('error'): return self.response['error'][0]['message'] elif self.errorMessage: @@ -197,19 +203,20 @@ class PaypalEnvelopeRequest: return None def envelope(self): - if self.response.has_key('responseEnvelope'): + + if self.response and self.response.has_key('responseEnvelope'): return self.response['responseEnvelope'] else: return None def correlation_id(self): - if self.response.has_key('responseEnvelope') and self.response['responseEnvelope'].has_key('correlationId'): + if self.response and self.response.has_key('responseEnvelope') and self.response['responseEnvelope'].has_key('correlationId'): return self.response['responseEnvelope']['correlationId'] else: return None def timestamp(self): - if self.response.has_key('responseEnvelope') and self.response['responseEnvelope'].has_key('timestamp'): + if self.response and self.response.has_key('responseEnvelope') and self.response['responseEnvelope'].has_key('timestamp'): return self.response['responseEnvelope']['timestamp'] else: return None @@ -532,6 +539,46 @@ class CancelPreapproval(PaypalEnvelopeRequest): except: traceback.print_exc() self.errorMessage = "Error: Server Error" + + +class RefundPayment(PaypalEnvelopeRequest): + + def __init__(self, transaction): + + try: + + headers = { + 'X-PAYPAL-SECURITY-USERID':settings.PAYPAL_USERNAME, + 'X-PAYPAL-SECURITY-PASSWORD':settings.PAYPAL_PASSWORD, + 'X-PAYPAL-SECURITY-SIGNATURE':settings.PAYPAL_SIGNATURE, + 'X-PAYPAL-APPLICATION-ID':settings.PAYPAL_APPID, + 'X-PAYPAL-REQUEST-DATA-FORMAT':'JSON', + 'X-PAYPAL-RESPONSE-DATA-FORMAT':'JSON', + } + + data = { + 'payKey':transaction.pay_key, + 'requestEnvelope': { 'errorLanguage': 'en_US' } + } + + self.raw_request = json.dumps(data) + self.headers = headers + self.url = "/AdaptivePayments/Refund" + self.connection = url_request(self) + self.code = self.connection.code() + + if self.code != 200: + self.errorMessage = 'PayPal response code was %i' % self.code + return + + self.raw_response = self.connection.content() + logger.info("paypal Refund response was: %s" % self.raw_response) + self.response = json.loads( self.raw_response ) + logger.info(self.response) + + except: + traceback.print_exc() + self.errorMessage = "Error: Server Error" class Preapproval( PaypalEnvelopeRequest ): diff --git a/payment/urls.py b/payment/urls.py index 0d7ac235..99856bf6 100644 --- a/payment/urls.py +++ b/payment/urls.py @@ -12,4 +12,6 @@ urlpatterns = patterns( url(r"^paymentcomplete","paymentcomplete"), url(r"^checkstatus", "checkStatus"), url(r"^testfinish", "testFinish"), + url(r"^testrefund", "testRefund"), + url(r"^testmodify", "testModify"), ) diff --git a/payment/views.py b/payment/views.py index 36426ab7..df4ca826 100644 --- a/payment/views.py +++ b/payment/views.py @@ -16,7 +16,7 @@ import logging logger = logging.getLogger(__name__) # parameterize some test recipients -TEST_RECEIVERS = ['jakace_1309677337_biz@gmail.com', 'seller_1317463643_biz@gmail.com'] +TEST_RECEIVERS = ['seller_1317463643_biz@gmail.com', 'Buyer6_1325742408_per@gmail.com'] #TEST_RECEIVERS = ['glueja_1317336101_biz@gluejar.com', 'rh1_1317336251_biz@gluejar.com', 'RH2_1317336302_biz@gluejar.com'] @@ -114,8 +114,8 @@ def testAuthorize(request): return HttpResponseRedirect(url) else: - response = t.reference - logger.info("testAuthorize: Error " + str(t.reference)) + response = t.error + logger.info("testAuthorize: Error " + str(t.error)) return HttpResponse(response) ''' @@ -136,6 +136,58 @@ def testCancel(request): message = "Error: " + t.error return HttpResponse(message) +''' +http://BASE/testrefund?transaction=2 + +Example that refunds a transaction +''' +def testRefund(request): + + if "transaction" not in request.GET.keys(): + return HttpResponse("No Transaction in Request") + + t = Transaction.objects.get(id=int(request.GET['transaction'])) + p = PaymentManager() + if p.refund_transaction(t): + return HttpResponse("Success") + else: + if t.error: + message = "Error: " + t.error + else: + message = "Error" + + return HttpResponse(message) + +''' +http://BASE/testmodufy?transaction=2 + +Example that modifies the amount of a transaction +''' +def testModify(request): + + if "transaction" not in request.GET.keys(): + return HttpResponse("No Transaction in Request") + + if "amount" in request.GET.keys(): + amount = float(request.GET['amount']) + else: + amount = 200.0 + + t = Transaction.objects.get(id=int(request.GET['transaction'])) + p = PaymentManager() + + status, url = p.modify_transaction(t, amount) + + if url: + logger.info("testModify: " + url) + return HttpResponseRedirect(url) + + if status: + return HttpResponse("Success") + else: + return HttpResponse("Error") + + ''' http://BASE/testfinish?transaction=2