class GenericPayerBackend(object): url_namespace = 'payer' backend_name = _('Payer') template = 'payer_backend/redirect.html' payment_methods = [ PAYMENT_METHOD_CARD, PAYMENT_METHOD_BANK, PAYMENT_METHOD_PHONE, PAYMENT_METHOD_INVOICE, ] def __init__(self, shop): self.shop = shop self.api = PayerPostAPI( agent_id=getattr(settings, 'SHOP_PAYER_BACKEND_AGENT_ID', ''), key_1=getattr(settings, 'SHOP_PAYER_BACKEND_ID1', ''), key_2=getattr(settings, 'SHOP_PAYER_BACKEND_ID2', ''), payment_methods=self.payment_methods, test_mode=getattr(settings, 'SHOP_PAYER_BACKEND_TEST_MODE', False), debug_mode=getattr(settings, 'SHOP_PAYER_BACKEND_DEBUG_MODE', DEBUG_MODE_SILENT), hide_details=getattr(settings, 'SHOP_PAYER_BACKEND_HIDE_DETAILS', False), ) for ip in getattr(settings, 'SHOP_PAYER_BACKEND_IP_WHITELIST', []): self.api.add_whitelist_ip(ip) for ip in getattr(settings, 'SHOP_PAYER_BACKEND_IP_BLACKLIST', []): self.api.add_blacklist_ip(ip) def get_url_name(self, name=None): if not name: return self.url_namespace return '%s-%s' % (self.url_namespace, name,) def get_urls(self): urlpatterns = patterns( '', url(r'^$', self.payer_redirect_view, name=self.get_url_name()), url(r'^authorize/$', self.callback_notification_view, name=self.get_url_name('authorize')), url(r'^settle/$', self.callback_notification_view, name=self.get_url_name('settle')), ) return urlpatterns def get_processing_control(self, request): return PayerProcessingControl( success_redirect_url=request.build_absolute_uri(self.shop.get_finished_url()), authorize_notification_url=request.build_absolute_uri(reverse(self.get_url_name('authorize'))), settle_notification_url=request.build_absolute_uri(reverse(self.get_url_name('settle'))), redirect_back_to_shop_url=request.build_absolute_uri(self.shop.get_cancel_url()), ) @on_method(shop_login_required) @on_method(order_required) def payer_redirect_view(self, request): """ This view generates the order XML data and other key-value pairs needed, outputs the as hidden input elemens in a form and redirects to Payer. """ order = self.shop.get_order(request) description = self.shop.get_order_short_name(order) order_id = self.shop.get_order_unique_id(order) user = None if request.user.is_authenticated(): user = request.user payer_order = PayerOrder( order_id=order_id, description=description, ) payer_order.set_buyer_details(buyer_details_from_user(user=user, order=order)) for order_item in order.items.all(): payer_order.add_order_item(payer_order_item_from_order_item(order_item)) for extra_order_price in order.extraorderpricefield_set.all(): payer_order.add_order_item(payer_order_item_from_extra_order_price(extra_order_price)) for info in order.extra_info.all(): for t in list(string_chunks(info.text, 255)): payer_order.add_info_line(t) self.api.set_processing_control(self.get_processing_control(request)) self.api.set_order(payer_order) redirect_data = self.api.get_post_data() form = PayerRedirectForm(redirect_data=redirect_data) ctx_data = { 'order': order, 'redirect_data': redirect_data, 'form_action': self.api.get_post_url(), 'form': form, } if settings.DEBUG: ctx_data['xml_data'] = self.api.get_xml_data(pretty_print=True) context = RequestContext(request, ctx_data) return render_to_response(self.template, context) def is_valid_remote_addr(self, request): def get_remote_addr(request): x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: ip = x_forwarded_for.split(',')[0] else: ip = request.META.get('REMOTE_ADDR') return ip try: remote_addr = get_remote_addr(request) return self.api.validate_callback_ip(remote_addr) except Exception as e: logging.error(u"IP address callback did not validate: %s" % unicode(e)) return False def is_valid_callback(self, request): try: url = request.build_absolute_uri(request.get_full_path()) return self.api.validate_callback_url(url) except Exception as e: logging.error(u"Callback URL did not validate: %s" % unicode(e)) return False def callback_notification_view(self, request): valid_callback = self.is_valid_remote_addr(request) and self.is_valid_callback(request) if valid_callback: try: self.handle_order_notifications(request.GET) except Exception as e: logging.error(u"Error handling order notification: %s" % unicode(e)) return HttpResponse("TRUE" if valid_callback else "FALSE", content_type="text/plain") def handle_order_notifications(self, data): order_id = data.get(PayerXMLDocument.ORDER_ID_URL_PARAMETER_NAME, data.get('payer_merchant_reference_id', None)) payment_method = data.get('payer_payment_type', 'unknown') transaction_id = data.get('payer_payment_id', data.get('payread_payment_id', None)) callback_type = data.get('payer_callback_type', '').lower() # testmode = bool(data.get('payer_testmode', 'false') == 'true') # added_fee = data.get('payer_added_fee', 0) if order_id is not None: order = Order.objects.get(pk=order_id) if callback_type == 'auth': # Payment approved, update order status, empty cart order.status = Order.CONFIRMED order.save() try: cart = Cart.objects.get(pk=order.cart_pk) cart.empty() except Cart.DoesNotExist: pass confirmed.send(sender=self, order=order) elif callback_type == 'settle': # Payment completed, update order status, add payment order.status = Order.COMPLETED self.shop.confirm_payment(order, self.shop.get_order_total(order), transaction_id, u"Payer %s (%s)" % (unicode(self.backend_name).lower(), payment_method,)) completed.send(sender=self, order=order)
class TestPayerPostAPI(TestCase): def setUp(self): self.api = PayerPostAPI( agent_id="AGENT_ID", key_1="6866ef97a972ba3a2c6ff8bb2812981054770162", key_2="1388ac756f07b0dda2961436ba8596c7b7995e94", ) self.api.set_processing_control(self.getProcessingControl()) self.api.set_order(self.getOrder()) def getOrder(self): return PayerOrder(order_id="123456", buyer_details=PayerBuyerDetails( first_name="John", last_name="Doe", address_line_1="1234 Main Street", postal_code="12345", city="Anywhere", phone_mobile="012345678", email="*****@*****.**", ), order_items=[ PayerOrderItem( description='A product', price_including_vat=123.50, vat_percentage=25, quantity=4, ), PayerOrderItem( description='Another product', price_including_vat=123.0, vat_percentage=12.5, quantity=2, ), ], info_lines=[ "Shipping with 5 work days", "Additional line of order info", ]) def getProcessingControl(self): return PayerProcessingControl( success_redirect_url="http://localhost/webshop/thankyou/", authorize_notification_url="http://localhost/webshop/auth/", settle_notification_url="http://localhost/webshop/settle/", redirect_back_to_shop_url="http://localhost/webshop/", ) def test_settings(self): self.assertEqual(self.api.get_post_url(), self.api.PAYER_POST_URL) self.assertEqual(self.api.get_post_url(), "https://secure.payer.se/PostAPI_V1/InitPayFlow") def test_checksums(self): def check_checksums(xml_data, b64_data): checksum = self.api.get_checksum(b64_data) expected_checksum = hashlib.md5( "6866ef97a972ba3a2c6ff8bb2812981054770162" + base64.b64encode(xml_data) + "1388ac756f07b0dda2961436ba8596c7b7995e94").hexdigest() self.assertEqual(checksum, expected_checksum) xml_data = self.api.get_xml_data() b64_data = self.api.get_base64_data() check_checksums(xml_data, b64_data) check_checksums("foo bar", self.api.get_base64_data("foo bar")) def test_callback_validation(self): xml = self.api.xml_document auth_url = xml.get_authorize_notification_url( order_id=self.api.order.order_id) settle_url = xml.get_settle_notification_url( order_id=self.api.order.order_id) ad = { 'payer_callback_type': 'auth', 'payer_testmode': 'false', 'payer_payment_type': 'card', 'payer_merchant_reference_id': self.api.order.order_id, } sd = ad sd.update({ 'payer_callback_type': 'settle', 'payer_added_fee': 0, 'payer_payment_id': 'yYkYOKFf_Z@hbJj3nS41Q2xE9dVm', 'payread_payment_id': 'yYkYOKFf_Z@hbJj3nS41Q2xE9dVm', }) # We avoid using urllib.erlencode() here as incoming URL's are known to # be erroneously encoded, e.g. enecoded @ characters in query string def add_query_params(url, params): qsl = ["%s=%s" % ( k, v, ) for k, v in params.iteritems()] return "&".join([url] + qsl) auth_url = add_query_params(auth_url, ad) settle_url = add_query_params(settle_url, sd) def get_md5_sum(url): return hashlib.md5( "6866ef97a972ba3a2c6ff8bb2812981054770162" + url + "1388ac756f07b0dda2961436ba8596c7b7995e94").hexdigest() auth_url_test = "%s&md5sum=%s" % (auth_url, get_md5_sum(auth_url)) auth_url_api = "%s&md5sum=%s" % (auth_url, self.api.get_checksum(auth_url)) settle_url_test = "%s&md5sum=%s" % (settle_url, get_md5_sum(settle_url)) settle_url_api = "%s&md5sum=%s" % (settle_url, self.api.get_checksum(settle_url)) self.assertTrue(self.api.validate_callback_url(auth_url_test)) self.assertTrue(self.api.validate_callback_url(auth_url_api)) self.assertTrue(self.api.validate_callback_url(settle_url_test)) self.assertTrue(self.api.validate_callback_url(settle_url_api)) self.api.suppress_validation_checks = True self.assertTrue(self.api.validate_callback_url("fake url")) self.api.suppress_validation_checks = False self.assertEqual(auth_url_test, auth_url_api) self.assertEqual(settle_url_test, settle_url_api) self.assertRaises(PayerURLValidationError, self.api.validate_callback_url, (self.api, auth_url)) self.assertRaises(PayerURLValidationError, self.api.validate_callback_url, (self.api, settle_url)) self.assertRaises( PayerURLValidationError, self.api.validate_callback_url, (self.api, auth_url + "&md5sum=79acb36d5a10837c377e6f3f1cf9fc9c")) self.assertRaises(PayerURLValidationError, self.api.validate_callback_url, (self.api, settle_url + "&md5sum=79acb36d5a10837c377e6f3f1cf9fc9c")) try: self.api.validate_callback_url( settle_url + "&md5sum=79acb36d5a10837c377e6f3f1cf9fc9c") except PayerURLValidationError as e: self.assertTrue(str(e).startswith("MD5 checksums did not match.")) # IP white/blacklists for ip in self.api.ip_whitelist: self.assertTrue(self.api.validate_callback_ip(ip)) new_ip = "123.123.123.123" self.assertRaises(PayerIPNotOnWhitelistException, self.api.validate_callback_ip, new_ip) self.api.suppress_validation_checks = True self.assertTrue(self.api.validate_callback_ip("fake ip")) self.api.suppress_validation_checks = False self.api.add_whitelist_ip(new_ip) self.assertTrue(self.api.validate_callback_ip(new_ip)) self.api.add_blacklist_ip(new_ip) self.api.add_blacklist_ip("234.234.234.234") for ip in self.api.ip_blacklist: self.assertRaises(PayerIPBlacklistedException, self.api.validate_callback_ip, ip) def test_config_errors(self): api = PayerPostAPI(agent_id=None, key_1=None, key_2=None) # Checksums (keys 1 & 2) self.assertRaises(PayerPostAPIError, api.get_checksum, "data") api.key_1 = "key1" self.assertRaises(PayerPostAPIError, api.get_checksum, "data") api.key_2 = "key2" raised = False try: api.get_checksum("data") except: raised = True try: api.get_post_data() except PayerPostAPIError as e: self.assertEqual(e.code, PayerPostAPIError.ERROR_MISSING_ORDER) api.set_order(self.getOrder()) self.assertFalse(raised, 'Exception raised') self.assertIsNotNone(api.get_checksum("data")) # Order, Processing control, agent ID self.assertRaises(PayerPostAPIError, api.get_post_data) try: api.get_post_data() except PayerPostAPIError as e: self.assertEqual( e.code, PayerPostAPIError.ERROR_MISSING_PROCESSING_CONTROL) self.assertEqual( str(e), repr("Error %s: %s" % (e.code, e.ERROR_MESSAGES.get(e.code, "Unknown Error")))) api.agent_id = "AGENT_ID" api.set_processing_control(self.getProcessingControl()) raised = False try: api.get_post_data() except: raised = True self.assertFalse(raised, 'Exception raised') api.agent_id = None self.assertRaises(PayerPostAPIError, api._generate_xml) def test_xml_error(self): data = self.api.get_xml_data() assert data self.api.xml_document = "Fake XML document" self.assertRaises(PayerPostAPIError, self.api.get_xml_data)