def get_payment_form(self, payment, locale=None, remote_addr=None): # Check whether we've "used" this payment already. We don't really need # this for MSP here, but in our current implementation, the user of the # payment API calls mark_submitted() (transfer_initiated=yes) on the # redirecting/form page. When called a second time, that will raise an # error. if payment.transfer_initiated: raise PaymentAlreadyUsed() # user clicked back? # (1) Start transaction by messaging MultiSafepay directly. result = self.start_transaction(payment, locale=locale, remote_addr=remote_addr) # result = '''<?xml version="1.0" encoding="UTF-8"?> # <redirecttransaction result="ok"> # <transaction> # <id>4039</id> # <payment_url>https://pay.multisafepay.com/pay/?transaction=12345&lang=nl_NL</payment_url> # </transaction> # </redirecttransaction>''' # OR # result = '''<?xml version="1.0" encoding="UTF-8"?> # <redirecttransaction result="error"> # <error> # <code>1006</code> # <description>Invalid transaction ID</description> # </error> # <transaction><id>4326</id></transaction> # </redirecttransaction>''' # (2) Fetch URL from results. try: dom = string2dom(result) # FIXME: ugly code error = dom.getElementsByTagName('error') if error: code = error[0].getElementsByTagName('code')[0] code = u''.join(i.wholeText for i in code.childNodes) desc = error[0].getElementsByTagName('description')[0] desc = u''.join(i.wholeText for i in desc.childNodes) if code == '1006': # Invalid transaction ID # User clicked back and a used payment was attempted # again, or it could simply be that the credentials are # bad. log('Credentials bad or payment already used', 'msp', 'err') raise PaymentAlreadyUsed() # user clicked back somehow? payment_url_node = dom.getElementsByTagName('payment_url')[0] payment_url = u''.join(i.wholeText for i in payment_url_node.childNodes) except PaymentAlreadyUsed: raise except: mail_admins('Error when parsing result', result) # XXX/TEMP raise # (3) Send user to URL. return self.create_html_form_from_url(payment_url, 'msp_form')
def get_redirect_url(self, payment_id): try: payment = Payment.objects.get(id=payment_id) except Payment.DoesNotExist as e: pass else: # We get transaction_id in the URL.. check it.. because we # can. transaction_id = (self.request.GET.get('transaction_id').encode( 'ascii', 'replace')) if str(payment.unique_key.rsplit('-', 1)[0]) == transaction_id: # If all is well, the transaction_report url has already # been called. That means there's either payment success # or payment failure. This isn't 100% proof though. # If status is still submitted/unknown, we must be prepared # to tell the user that that is the case. if payment.is_success is None: next_url = payment.get_url('toosoon') elif payment.is_success: next_url = payment.get_url('success') else: next_url = payment.get_url('abort') return next_url else: e = ValueError('Mismatch of transaction_id in GET') mail_admins( u'Check failed at mollie/ideal transaction_return', u'Exception: %s (%r)\n\nGet: %r\n\nPost: %r\n\nMeta: %r' % (e, e, self.request.GET, self.request.POST, self.request.META)) return '/?fail'
def get_redirect_url(self, payment_id): # Check that the user is logged in and that the payment belongs to # said user. If we skip this we can get user-trailing-bots hitting # this URL without a query_string, resulting in an assert-fail below. user = self.request.user if user.is_anonymous(): raise Http404() # not logged in try: payment = Payment.objects.get(id=payment_id) except Payment.DoesNotExist: raise Http404() # does not exist if payment.paying_user_id != user.id: raise Http404() # belongs to different user get = self.request.GET paypal = get_instance() try: paypal.process_aborted(payment, get['token']) except Exception as e: pass else: # Return to a abort url where we can wipe our tears return payment.get_url('abort') mail_admins( u'Check failed at paypal/paypal transaction_aborted', u'Exception: %s (%r)\n\nGet: %r\n\nPost: %r\n\nMeta: %r' % (e, e, self.request.GET, self.request.POST, self.request.META)) return '/?fail'
def post(self, request, payment_id): self.check_remote_addr(request) log(repr(request.POST), 'targetpay', 'report') content_type = 'text/plain; charset=UTF-8' try: payment = Payment.objects.get(id=payment_id) if payment.unique_key: trxid = payment.unique_key.split('-', 1)[1] if request.POST.get('trxid') != trxid: raise Payment.DoesNotExist('bad trxid?') elif request.POST.get('status') == 'Expired': # Since aug2018, TGP started sending Expired notices for # 3-hour old transactions that weren't picked up. # Mark transaction as failed, and answer with OK. payment.mark_aborted() return HttpResponse('OK', content_type=content_type) except Payment.DoesNotExist as e: mail_admins('Check failed at TGP TransactionReport', (u'Exception: %s (%r)\n\nGet: %r\n\nPost: %r\n\n' u'Meta: %r' % (e, e, request.GET, request.POST, request.META))) response = HttpResponse('NAK', content_type=content_type) response.status_code = 500 return response provider_sub = payment.unique_key.split('-', 1)[0] targetpay = get_instance(provider_sub) try: targetpay.request_status(payment, request) except Exception as e: if isinstance(e, AtomicUpdateDupe): # "duplicate status" status, reply = 200, 'OK' else: status, reply = 500, 'NAK' payinfo = { 'id': payment.id, 'created': payment.created, 'is_success': payment.is_success, 'blob': payment.blob, } mail_admins( (u'Replying with %s to TGP report [%s, %s, %s] ' u'(might indicate a problem)' % (reply, payment.id, payment.is_success, payment.created)), (u'Exception: %s (%s)\n\nGet: %r\n\nPost: %r\n\n' u'Traceback: %s\n\nMeta: %r\n\nPayment: %r' % (e, e, request.GET, request.POST, traceback.format_exc(), request.META, payinfo))) response = HttpResponse(reply, content_type=content_type) response.status_code = status return response return HttpResponse('OK', content_type=content_type)
def _do_request(self, template, extra_kwargs): kwargs = { 'ua': self.ua, 'account': self.account, 'site_id': self.site_id, 'site_secure_code': self.site_secure_code, } kwargs.update(extra_kwargs) body = template % kwargs headers = { 'Content-Type': 'text/xml; charset=UTF-8', 'Accept': '*/*', 'User-Agent': self.ua, } # Apparently we get SSL handshake timeouts after 1m52. That's too # long. Try and reduce that by adding a defaulttimeout. Only if that # works can be start adding a retry. # NOTE: http://hg.python.org/cpython/rev/3d9a86b8cc98/ # NOTE: http://hg.python.org/cpython/rev/ce4916ca06dd/ socket.setdefaulttimeout(20) timeout_seconds = 5 request = urllib2.Request(self.api_url, data=body.encode('utf-8'), headers=headers) log(body, 'msp', 'out') try: response = urllib2.urlopen(request, timeout=timeout_seconds) except urllib2.HTTPError as e: # TEMP: this could use some tweaking contents = e.read() mail_admins( 'Incoming error to this XML', body + '\n\n--\n' + contents + '\n\nURL: ' + self.api_url + '\n') log('Got error %s with response: %s' % (e.code, contents), 'msp', 'error') raise except Exception as e: # TEMP: this could use some tweaking mail_admins('Incoming error to this XML', body + '\n\nURL: ' + self.api_url + '\n') log('Got error: %s' % (e, ), 'msp', 'error') raise else: data = response.read() log(data, 'msp', 'in') return data
def get_redirect_url(self, payment_id, transaction_key): try: payment = Payment.objects.get(id=payment_id) except Payment.DoesNotExist as e: pass else: sofort = get_instance() try: sofort.process_aborted(payment, transaction_key) except Exception as e: pass else: # Return to a abort url where we can wipe our tears return payment.get_url('abort') mail_admins( u'Check failed at sofort/ideal transaction_aborted', u'Exception: %s (%r)\n\nGet: %r\n\nPost: %r\n\nMeta: %r' % (e, e, self.request.GET, self.request.POST, self.request.META)) return '/?fail'
def get_redirect_url(self, payment_id): # Check that the user is logged in and that the payment belongs to # said user. If we skip this we can get user-trailing-bots hitting # this URL without a query_string, resulting in an assert-fail below. user = self.request.user if user.is_anonymous(): raise Http404() # not logged in try: payment = Payment.objects.get(id=payment_id) except Payment.DoesNotExist: raise Http404() # does not exist if payment.paying_user_id != user.id: raise Http404() # belongs to different user get = self.request.GET paypal = get_instance() try: paypal.process_passed(payment, get['token'], get['PayerID']) except ProviderError: # If there is a provider error, we may want to catch # said error and inform the user. raise except TryDifferentPayment: # The user should try a different payment method. It's # customary to catch this (and the ProviderError) from # an exception handling middleware. raise except Exception as e: # If there is an unknown error, we won't shove a 500 in # the user's face but mail the admins while we send the # user to '/'. pass else: # Return to a success url where we can celebrate return payment.get_url('success') mail_admins( u'Check failed at paypal/paypal transaction_passed', u'Exception: %s (%r)\n\nGet: %r\n\nPost: %r\n\nMeta: %r' % (e, e, self.request.GET, self.request.POST, self.request.META)) return '/?fail'
def get(self, request, payment_id): content_type = 'text/plain; charset=UTF-8' try: payment = Payment.objects.get(id=payment_id) except Payment.DoesNotExist as e: pass else: transaction_id = self.request.GET.get('transaction_id') mollie = get_instance() try: mollie.process_report(payment, transaction_id) except Exception as e: # XXX/FIXME: here we should: # (a) do the lookup # (b) if the lookup says 'aborted', we can rest # (c) if the lookup says 'paid', we have a problem mail_admins( ('Replying with NAK to bad message from Mollie ' '(might indicate a problem)'), (u'Exception: %s (%s)\n\nGet: %r\n\nPost: %r\n\n' u'Meta: %r' % (e, e, self.request.GET, self.request.POST, self.request.META))) response = HttpResponse('NAK', content_type=content_type) response.status_code = 500 return response else: return HttpResponse('OK', content_type=content_type) mail_admins( u'Check failed at mollie/ideal transaction_report', (u'Exception: %s (%s)\n\nGet: %r\n\nPost: %r\n\nMeta: %r' % (e, e, self.request.GET, self.request.POST, self.request.META))) response = HttpResponse('NAK', content_type=content_type) response.status_code = 500 return response
def get(self, request): content_type = 'text/plain; charset=UTF-8' payment_id = request.GET.get('transactionid') try: payment = Payment.objects.get(id=payment_id) except Payment.DoesNotExist as e: pass else: msp = get_instance() try: msp.request_status(payment) except Exception as e: if isinstance(e, ProviderIsInactive): # "provider account is inactive"; cope with MSP # sending reports even though they don't accept us # requesting the necessary info. status, reply = 200, 'OK' else: status, reply = 500, 'NAK' payinfo = { 'id': payment.id, 'created': payment.created, 'is_success': payment.is_success, 'blob': payment.blob, } mail_admins((u'Replying with %s to MSP report [%s, %s, %s] ' u'(might indicate a problem)' % ( reply, payment.id, payment.is_success, payment.created)), (u'Exception: %s (%s)\n\nGet: %r\n\nPost: %r\n\n' u'Traceback: %s\n\nMeta: %r\n\nPayment: %r' % ( e, e, request.GET, request.POST, traceback.format_exc(), request.META, payinfo))) response = HttpResponse(reply, content_type=content_type) response.status_code = status return response else: return HttpResponse('OK', content_type=content_type) # WTF is Apple doing? Ignore requests that have no transactionid. # User-Agent: # Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 # (KHTML, like Gecko) Chrome/31.0.1623.0 Safari/537.36" # Referer: # (nothing) # Requests: # 17.142.152.15 - - [09/Jan/2015:01:49:47 +0100] # "GET /api/msp/report HTTP/1.1" 301 0 # 17.142.152.15 - - [09/Jan/2015:01:49:48 +0100] # "GET /api/msp/report/ HTTP/1.1" 500 3 # 17.142.152.15 - - [09/Jan/2015:01:49:48 +0100] # "GET /api/msp/report/ HTTP/1.1" 500 3 # 17.142.149.128 - - [10/Jan/2015:04:28:43 +0100] # "GET /api/msp/report HTTP/1.1" 301 0 # 17.142.149.128 - - [10/Jan/2015:04:28:46 +0100] # "GET /api/msp/report/ HTTP/1.1" 500 3 # 17.142.149.128 - - [10/Jan/2015:04:28:46 +0100] # "GET /api/msp/report/ HTTP/1.1" 500 3 # 17.142.148.235 - - [10/Jan/2015:10:49:36 +0100] # "GET /api/msp/report HTTP/1.1" 301 0 # 17.142.148.235 - - [10/Jan/2015:10:49:37 +0100] # "GET /api/msp/report/ HTTP/1.1" 500 3 # 17.142.148.235 - - [10/Jan/2015:10:49:38 +0100] # "GET /api/msp/report/ HTTP/1.1" 500 3 # 17.142.151.101 - - [10/Jan/2015:18:33:14 +0100] # "GET /api/msp/report HTTP/1.1" 301 0 # 17.142.151.101 - - [10/Jan/2015:18:33:15 +0100] # "GET /api/msp/report/ HTTP/1.1" 500 3 # 17.142.151.101 - - [10/Jan/2015:18:33:15 +0100] # "GET /api/msp/report/ HTTP/1.1" 500 3 # That's in the apple range: # 17.0.0.0/8 (APPLE-WWNET), the hosts don't have a PTR. # This is supposed to be an internal URI for the MSP provider # only... if payment_id: mail_admins('Check failed at msp/msp transaction_report', (u'Exception: %s (%r)\n\nGet: %r\n\nPost: %r\n\n' u'Meta: %r' % (e, e, request.GET, request.POST, request.META))) response = HttpResponse('NAK', content_type=content_type) response.status_code = 500 return response
def process_passed(self, payment, token, payer_id): """ At this point we do not have the payment, but we're allowed to initiate it. """ # We stored the token in the unique_key. Get it from # payment.unique_key directly instead of using get_unique_key(). # Otherwise we might be creating a bogus unique_key which we # won't be using. if str(payment.unique_key).rsplit('-', 1)[0] != str(token): raise PaymentSuspect( 'Found token %s differing from payment %s' % (token, payment.id)) if len(token) < 16: raise PaymentSuspect( 'Found invalid token %s in payment %s' % (token, payment.id)) if payment.is_success is not None: raise PaymentSuspect( 'Got payment status report for %s which is already %s' % (payment.id, payment.is_success)) # Continue and poke Paypal to make the payment. # We do have the option to call GetExpressCheckoutDetails first # to see what the user-provided parameters are. But, we don't # care about any of those. Just give us the money. params = { # API options 'METHOD': 'DoExpressCheckoutPayment', # Mandatory payment info 'PAYMENTREQUEST_0_AMT': '%.2f' % (payment.get_amount(),), 'PAYMENTREQUEST_0_CURRENCYCODE': 'EUR', # we're in the EU.. # Mandatory verification info 'TOKEN': token, 'PAYERID': payer_id, # Optional notification URL (IPN): we may implement this # later, but I believe we need to have HTTPS enabled for # this to work on live. # 'PAYMENTREQUEST_0_NOTIFYURL: 'http://example.com/notify.html', } # Do request response = self._do_request(params) # DoExpressCheckoutPayment returns something like: # TOKEN=EC%2d3EM7309999999999T&SUCCESSPAGEREDIRECTREQUESTED=true # &TIMESTAMP=2012%2d03%2d24T23%3a08%3a10Z&CORRELATIONID=abcdef1234567 # &ACK=Success&VERSION=78&BUILD=2649250&INSURANCEOPTIONSELECTED=false # &SHIPPINGOPTIONISDEFAULT=false # &PAYMENTINFO_0_TRANSACTIONID=11U999999E777777D # &PAYMENTINFO_0_RECEIPTID=1111%2d2222%2d3333%2d4444 # &PAYMENTINFO_0_TRANSACTIONTYPE=expresscheckout # &PAYMENTINFO_0_PAYMENTTYPE=instant # &PAYMENTINFO_0_ORDERTIME=2012%2d03%2d24T23%3a08%3a07Z # &PAYMENTINFO_0_AMT=10%2e00&PAYMENTINFO_0_FEEAMT=0%2e69 # &PAYMENTINFO_0_TAXAMT=0%2e00&PAYMENTINFO_0_CURRENCYCODE=EUR # &PAYMENTINFO_0_PAYMENTSTATUS=Completed # &PAYMENTINFO_0_PENDINGREASON=None&PAYMENTINFO_0_REASONCODE=None # &PAYMENTINFO_0_PROTECTIONELIGIBILITY=Ineligible # &PAYMENTINFO_0_PROTECTIONELIGIBILITYTYPE=None # &PAYMENTINFO_0_SECUREMERCHANTACCOUNTID=ABC123XYZZZZZ # &PAYMENTINFO_0_ERRORCODE=0&PAYMENTINFO_0_ACK=Success # There is lots of nice info in there. We store all of it for # later viewing in the blob. payment.set_blob('paypal: ' + repr(response)) # Get the actual payment status is_paid = (response['PAYMENTINFO_0_ACK'] == 'Success') if is_paid: # Payment went well payment.mark_passed() # This, is a bit of a question mark though. At the moment, # there is no way for us to get more status if the deal # wasn't really sealed. We add some logging and hope for # the best. if (response['PAYMENTINFO_0_PAYMENTTYPE'] != 'instant' or (decimal.Decimal(response['PAYMENTINFO_0_AMT']) != payment.get_amount()) or response['PAYMENTINFO_0_PAYMENTSTATUS'] != 'Completed'): if mail_admins: mail_admins( ('Paypal payment %s not completed fully?' % (payment.id,)), repr(response) ) # Set payment status to succeeded payment.mark_succeeded() # Signal that something has happened payment_updated.send(sender=payment, change='passed') else: # This, is also a bit of a question mark. This is not # supposed to fail... will it autocorrect itself? if mail_admins: mail_admins( ('Paypal payment %s not completed fully? (2)' % (payment.id,)), repr(response) ) # Set payment status to aborted payment.mark_aborted() # Signal that something has happened payment_updated.send(sender=payment, change='aborted')