From 7a0f2e40c7a80d432acc115f8f56387a78918af0 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 6 Oct 2017 21:39:01 +0200 Subject: [PATCH 1/7] split logic and model, replace address data with function --- payments/models.py | 133 +++++++++++++++++++++++------------------ payments/testcommon.py | 59 ++++++++++++++++++ payments/utils.py | 54 +++++++++++++++++ 3 files changed, 187 insertions(+), 59 deletions(-) create mode 100644 payments/testcommon.py diff --git a/payments/models.py b/payments/models.py index 2cbd8206a..f4266176f 100644 --- a/payments/models.py +++ b/payments/models.py @@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _ from .core import provider_factory +from .utils import add_prefixed_address, getter_prefixed_address from . import FraudStatus, PaymentStatus @@ -32,51 +33,8 @@ def __setattr__(self, key, value): self._payment.extra_data = json.dumps(data) -class BasePayment(models.Model): - ''' - Represents a single transaction. Each instance has one or more PaymentItem. - ''' - variant = models.CharField(max_length=255) - #: Transaction status - status = models.CharField( - max_length=10, choices=PaymentStatus.CHOICES, - default=PaymentStatus.WAITING) - fraud_status = models.CharField( - _('fraud check'), max_length=10, choices=FraudStatus.CHOICES, - default=FraudStatus.UNKNOWN) - fraud_message = models.TextField(blank=True, default='') - #: Creation date and time - created = models.DateTimeField(auto_now_add=True) - #: Date and time of last modification - modified = models.DateTimeField(auto_now=True) - #: Transaction ID (if applicable) - transaction_id = models.CharField(max_length=255, blank=True) - #: Currency code (may be provider-specific) - currency = models.CharField(max_length=10) - #: Total amount (gross) - total = models.DecimalField(max_digits=9, decimal_places=2, default='0.0') - delivery = models.DecimalField( - max_digits=9, decimal_places=2, default='0.0') - tax = models.DecimalField(max_digits=9, decimal_places=2, default='0.0') - description = models.TextField(blank=True, default='') - billing_first_name = models.CharField(max_length=256, blank=True) - billing_last_name = models.CharField(max_length=256, blank=True) - billing_address_1 = models.CharField(max_length=256, blank=True) - billing_address_2 = models.CharField(max_length=256, blank=True) - billing_city = models.CharField(max_length=256, blank=True) - billing_postcode = models.CharField(max_length=256, blank=True) - billing_country_code = models.CharField(max_length=2, blank=True) - billing_country_area = models.CharField(max_length=256, blank=True) - billing_email = models.EmailField(blank=True) - customer_ip_address = models.GenericIPAddressField(blank=True, null=True) - extra_data = models.TextField(blank=True, default='') - message = models.TextField(blank=True, default='') - token = models.CharField(max_length=36, blank=True, default='') - captured_amount = models.DecimalField( - max_digits=9, decimal_places=2, default='0.0') - - class Meta: - abstract = True +class BasePaymentLogic(object): + """ Logic of a Payment object, e.g. for tests """ def change_status(self, status, message=''): ''' @@ -99,20 +57,8 @@ def change_fraud_status(self, status, message='', commit=True): if commit: self.save() - def save(self, **kwargs): - if not self.token: - tries = {} # Stores a set of tried values - while True: - token = str(uuid4()) - if token in tries and len(tries) >= 100: # After 100 tries we are impliying an infinite loop - raise SystemExit('A possible infinite loop was detected') - else: - if not self.__class__._default_manager.filter(token=token).exists(): - self.token = token - break - tries.add(token) - - return super(BasePayment, self).save(**kwargs) + def __str__(self): + return self.variant def __unicode__(self): return self.variant @@ -130,6 +76,14 @@ def get_failure_url(self): def get_success_url(self): raise NotImplementedError() + # needs to be implemented, see BasePaymentWithAddress for an example + def get_shipping_address(self): + raise NotImplementedError() + + # needs to be implemented, see BasePaymentWithAddress for an example + def get_billing_address(self): + raise NotImplementedError() + def get_process_url(self): return reverse('process_payment', kwargs={'token': self.token}) @@ -166,6 +120,67 @@ def refund(self, amount=None): self.change_status(PaymentStatus.REFUNDED) self.save() + def create_token(self): + if not self.token: + tries = {} # Stores a set of tried values + while True: + token = str(uuid4()) + if token in tries and len(tries) >= 100: # After 100 tries we are impliying an infinite loop + raise SystemExit('A possible infinite loop was detected') + else: + if not self.__class__._default_manager.filter(token=token).exists(): + self.token = token + break + tries.add(token) + @property def attrs(self): return PaymentAttributeProxy(self) + +class BasePayment(models.Model, BasePaymentLogic): + ''' + Represents a single transaction. Each instance has one or more PaymentItem. + ''' + variant = models.CharField(max_length=255) + #: Transaction status + status = models.CharField( + max_length=10, choices=PaymentStatus.CHOICES, + default=PaymentStatus.WAITING) + fraud_status = models.CharField( + _('fraud check'), max_length=10, choices=FraudStatus.CHOICES, + default=FraudStatus.UNKNOWN) + fraud_message = models.TextField(blank=True, default='') + #: Creation date and time + created = models.DateTimeField(auto_now_add=True) + #: Date and time of last modification + modified = models.DateTimeField(auto_now=True) + #: Transaction ID (if applicable) + transaction_id = models.CharField(max_length=255, blank=True) + #: Currency code (may be provider-specific) + currency = models.CharField(max_length=10) + #: Total amount (gross) + total = models.DecimalField(max_digits=9, decimal_places=2, default='0.0') + delivery = models.DecimalField( + max_digits=9, decimal_places=2, default='0.0') + tax = models.DecimalField(max_digits=9, decimal_places=2, default='0.0') + description = models.TextField(blank=True, default='') + billing_email = models.EmailField(blank=True) + customer_ip_address = models.GenericIPAddressField(blank=True, null=True) + extra_data = models.TextField(blank=True, default='') + message = models.TextField(blank=True, default='') + token = models.CharField(max_length=36, blank=True, default='') + captured_amount = models.DecimalField( + max_digits=9, decimal_places=2, default='0.0') + + class Meta: + abstract = True + + def save(self, **kwargs): + self.create_token() + return super(BasePayment, self).save(**kwargs) + +@add_prefixed_address("billing") +class BasePaymentWithAddress(BasePayment): + """ Has real billing address + shippingaddress alias on billing address (alias for backward compatibility) """ + get_billing_address = getter_prefixed_address("billing") + get_shipping_address = get_billing_address diff --git a/payments/testcommon.py b/payments/testcommon.py new file mode 100644 index 000000000..37c9e75d4 --- /dev/null +++ b/payments/testcommon.py @@ -0,0 +1,59 @@ + +from decimal import Decimal +from .models import BasePaymentLogic +from . import PaymentStatus, PurchasedItem +from .utils import getter_prefixed_address +from datetime import datetime + +def create_test_payment(**_kwargs): + class TestPayment(BasePaymentLogic): + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + id = 523 + pk = id + description = 'payment' + currency = 'USD' + delivery = Decimal(10.8) + status = PaymentStatus.WAITING + message = "" + tax = Decimal(10) + token = "undefined" + total = Decimal(100) + captured_amount = Decimal("0.0") + extra_data = "" + variant = "342" + transaction_id = "" + created = datetime.now() + modified = datetime.now() + + billing_first_name = 'John' + billing_last_name = 'Smith' + billing_address_1 = 'JohnStreet 23' + billing_address_2 = '' + billing_city = 'Neches' + billing_postcode = "75779" + billing_country_code = "US" + billing_country_area = "Tennessee" + billing_email = "example@example.com" + + customer_ip_address = "192.78.6.6" + + def get_purchased_items(self): + return [ + PurchasedItem( + name='foo', quantity=Decimal('10'), price=Decimal('20'), + currency='USD', sku='bar')] + + def get_failure_url(self): + return 'http://cancel.com' + + def get_process_url(self): + return 'http://example.com' + + def get_success_url(self): + return 'http://success.com' + + def save(self): + return self + TestPayment.__dict__.update(_kwargs) + return TestPayment diff --git a/payments/utils.py b/payments/utils.py index 2d78659e0..d14de2dcd 100644 --- a/payments/utils.py +++ b/payments/utils.py @@ -1,6 +1,8 @@ from datetime import date +import re from django.utils.translation import ugettext_lazy as _ +from django.db import models def get_month_choices(): @@ -12,3 +14,55 @@ def get_year_choices(): year_choices = [(str(x), str(x)) for x in range( date.today().year, date.today().year + 15)] return [('', _('Year'))] + year_choices + +_extract_streetnr = re.compile(r"([0-9]+)\s*$") +def extract_streetnr(address, fallback=None): + ret = _extract_streetnr.findall(address) + if ret: + return ret[0] + else: + return fallback + +def getter_prefixed_address(prefix): + """ create getter for prefixed address format """ + first_name = "{}_first_name".format(prefix) + last_name = "{}_last_name".format(prefix) + address_1 = "{}_address_1".format(prefix) + address_2 = "{}_address_2".format(prefix) + city = "{}_city".format(prefix) + postcode = "{}_postcode".format(prefix) + country_code = "{}_country_code".format(prefix) + country_area = "{}_country_area".format(prefix) + def _get_address(self): + return { + "first_name": getattr(self, first_name, None), + "last_name": getattr(self, last_name, None), + "address_1": getattr(self, address_1, None), + "address_2": getattr(self, address_2, None), + "city": getattr(self, city, None), + "postcode": getattr(self, postcode, None), + "country_code": getattr(self, country_code, None), + "country_area": getattr(self, country_area, None)} + return _get_address + +def add_prefixed_address(prefix): + """ add address with prefix to class """ + first_name = "{}_first_name".format(prefix) + last_name = "{}_last_name".format(prefix) + address_1 = "{}_address_1".format(prefix) + address_2 = "{}_address_2".format(prefix) + city = "{}_city".format(prefix) + postcode = "{}_postcode".format(prefix) + country_code = "{}_country_code".format(prefix) + country_area = "{}_country_area".format(prefix) + def class_to_customize(dclass): + setattr(dclass, first_name, models.CharField(max_length=256, blank=True)) + setattr(dclass, last_name, models.CharField(max_length=256, blank=True)) + setattr(dclass, address_1, models.CharField(max_length=256, blank=True)) + setattr(dclass, address_2, models.CharField(max_length=256, blank=True)) + setattr(dclass, city, models.CharField(max_length=256, blank=True)) + setattr(dclass, postcode, models.CharField(max_length=256, blank=True)) + setattr(dclass, country_code, models.CharField(max_length=2, blank=True)) + setattr(dclass, country_area, models.CharField(max_length=256, blank=True)) + return dclass + return class_to_customize From 0dc734e2e5bc629d9c07e37af666bc508ee0f2e1 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 6 Oct 2017 23:05:23 +0200 Subject: [PATCH 2/7] split address into get_billing_address, get_shipping_address functions (allows much more freedom) and adjust all providers for the change use create_test_payment for tests, TestPayment is now a Mock use unittest mocks when python3 --- payments/authorizenet/__init__.py | 18 ++++----- payments/authorizenet/test_authorizenet.py | 27 +++---------- payments/braintree/forms.py | 22 ++++++----- payments/braintree/test_braintree.py | 27 +++---------- payments/coinbase/test_coinbase.py | 35 +++------------- payments/cybersource/__init__.py | 17 ++++---- payments/cybersource/test_cybersource.py | 42 +++++--------------- payments/dotpay/test_dotpay.py | 25 +++--------- payments/dummy/test_dummy.py | 24 +---------- payments/models.py | 3 ++ payments/paypal/test_paypal.py | 46 +++++----------------- payments/sagepay/__init__.py | 37 +++++++++-------- payments/sagepay/test_sagepay.py | 28 +++---------- payments/sofort/__init__.py | 19 ++++----- payments/sofort/test_sofort.py | 30 ++++---------- payments/stripe/forms.py | 18 +++++---- payments/stripe/test_stripe.py | 45 +++------------------ payments/test_core.py | 5 ++- payments/testcommon.py | 34 ++++++++++++---- payments/wallet/test_wallet.py | 38 ++++-------------- setup.py | 13 +++--- 21 files changed, 182 insertions(+), 371 deletions(-) diff --git a/payments/authorizenet/__init__.py b/payments/authorizenet/__init__.py index 6a94cfde7..50dcdb7ef 100644 --- a/payments/authorizenet/__init__.py +++ b/payments/authorizenet/__init__.py @@ -23,19 +23,19 @@ def __init__(self, login_id, transaction_key, 'Authorize.Net does not support pre-authorization.') def get_transactions_data(self, payment): - data = { + billing = payment.get_billing_address() + return { 'x_amount': payment.total, 'x_currency_code': payment.currency, 'x_description': payment.description, - 'x_first_name': payment.billing_first_name, - 'x_last_name': payment.billing_last_name, - 'x_address': "%s, %s" % (payment.billing_address_1, - payment.billing_address_2), - 'x_city': payment.billing_city, - 'x_zip': payment.billing_postcode, - 'x_country': payment.billing_country_area + 'x_first_name': billing["first_name"], + 'x_last_name': billing["last_name"], + 'x_address': "%s, %s" % (billing["address_1"], + billing["address_2"]), + 'x_city': billing["city"], + 'x_zip': billing["postcode"], + 'x_country': billing["country_area"] } - return data def get_product_data(self, payment, extra_data=None): data = self.get_transactions_data(payment) diff --git a/payments/authorizenet/test_authorizenet.py b/payments/authorizenet/test_authorizenet.py index d70acd1dd..deb711a63 100644 --- a/payments/authorizenet/test_authorizenet.py +++ b/payments/authorizenet/test_authorizenet.py @@ -1,9 +1,13 @@ from __future__ import unicode_literals from unittest import TestCase -from mock import patch, MagicMock, Mock +try: + from unittest.mock import patch, MagicMock +except ImportError: + from mock import patch, MagicMock from . import AuthorizeNetProvider from .. import PaymentStatus, RedirectNeeded +from ..testcommon import create_test_payment LOGIN_ID = 'abcd1234' @@ -18,26 +22,7 @@ STATUS_CONFIRMED = '1' -class Payment(Mock): - id = 1 - variant = 'authorizenet' - currency = 'USD' - total = 100 - status = PaymentStatus.WAITING - transaction_id = None - captured_amount = 0 - - def get_process_url(self): - return 'http://example.com' - - def get_failure_url(self): - return 'http://cancel.com' - - def get_success_url(self): - return 'http://success.com' - - def change_status(self, status): - self.status = status +Payment = create_test_payment() diff --git a/payments/braintree/forms.py b/payments/braintree/forms.py index e52bef2d0..9d64f6219 100644 --- a/payments/braintree/forms.py +++ b/payments/braintree/forms.py @@ -41,20 +41,22 @@ def get_credit_card_clean_data(self): 'expiration_year': self.cleaned_data.get('expiration').year} def get_billing_data(self): + billing = self.payment.get_billing_address() return { - 'first_name': self.payment.billing_first_name, - 'last_name': self.payment.billing_last_name, - 'street_address': self.payment.billing_address_1, - 'extended_address': self.payment.billing_address_2, - 'locality': self.payment.billing_city, - 'region': self.payment.billing_country_area, - 'postal_code': self.payment.billing_postcode, - 'country_code_alpha2': self.payment.billing_country_code} + 'first_name': billing["first_name"], + 'last_name': billing["last_name"], + 'street_address': billing["address_1"], + 'extended_address': billing["address_2"], + 'locality': billing["city"], + 'region': billing["country_area"], + 'postal_code': billing["postcode"], + 'country_code_alpha2': billing["country_code"]} def get_customer_data(self): + billing = self.payment.get_billing_address() return { - 'first_name': self.payment.billing_first_name, - 'last_name': self.payment.billing_last_name} + 'first_name': billing["first_name"], + 'last_name': billing["last_name"]} def save(self): braintree.Transaction.submit_for_settlement(self.transaction_id) diff --git a/payments/braintree/test_braintree.py b/payments/braintree/test_braintree.py index 00ad7d00d..71e4b368e 100644 --- a/payments/braintree/test_braintree.py +++ b/payments/braintree/test_braintree.py @@ -1,9 +1,13 @@ from __future__ import unicode_literals from unittest import TestCase -from mock import patch, MagicMock, Mock +try: + from unittest.mock import patch, MagicMock +except ImportError: + from mock import patch, MagicMock from . import BraintreeProvider from .. import PaymentStatus, RedirectNeeded +from ..testcommon import create_test_payment MERCHANT_ID = 'test11' @@ -18,26 +22,7 @@ 'cvv2': '1234'} -class Payment(Mock): - id = 1 - variant = 'braintree' - currency = 'USD' - total = 100 - status = PaymentStatus.WAITING - transaction_id = None - captured_amount = 0 - - def get_process_url(self): - return 'http://example.com' - - def get_failure_url(self): - return 'http://cancel.com' - - def get_success_url(self): - return 'http://success.com' - - def change_status(self, status): - self.status = status +Payment = create_test_payment() class TestBraintreeProvider(TestCase): diff --git a/payments/coinbase/test_coinbase.py b/payments/coinbase/test_coinbase.py index e694a525b..5a8d11240 100644 --- a/payments/coinbase/test_coinbase.py +++ b/payments/coinbase/test_coinbase.py @@ -4,12 +4,16 @@ import json from decimal import Decimal from unittest import TestCase +try: + from unittest.mock import patch, MagicMock +except ImportError: + from mock import patch, MagicMock from django.http import HttpResponse, HttpResponseForbidden -from mock import MagicMock, patch from .. import PaymentStatus from . import CoinbaseProvider +from ..testcommon import create_test_payment PAYMENT_TOKEN = '5a4dae68-2715-4b1e-8bb2-2c2dbe9255f6' KEY = 'abc123' @@ -23,34 +27,7 @@ PAYMENT_TOKEN, KEY)).encode('utf-8')).hexdigest()}} -class Payment(object): - - id = 1 - description = 'payment' - currency = 'BTC' - total = Decimal(100) - status = PaymentStatus.WAITING - token = PAYMENT_TOKEN - variant = VARIANT - - def change_status(self, status): - self.status = status - - def get_failure_url(self): - return 'http://cancel.com' - - def get_process_url(self): - return 'http://example.com' - - def get_purchased_items(self): - return [] - - def save(self): - return self - - def get_success_url(self): - return 'http://success.com' - +Payment = create_test_payment(variant=VARIANT, token=PAYMENT_TOKEN, description='payment', currency='BTC', total=Decimal(100)) class TestCoinbaseProvider(TestCase): diff --git a/payments/cybersource/__init__.py b/payments/cybersource/__init__.py index 6b119ebf2..ae2da808c 100644 --- a/payments/cybersource/__init__.py +++ b/payments/cybersource/__init__.py @@ -366,15 +366,16 @@ def _prepare_card_data(self, data): return card def _prepare_billing_data(self, payment): + _billing_address = payment.get_billing_address() billing = self.client.factory.create('data:BillTo') - billing.firstName = payment.billing_first_name - billing.lastName = payment.billing_last_name - billing.street1 = payment.billing_address_1 - billing.street2 = payment.billing_address_2 - billing.city = payment.billing_city - billing.postalCode = payment.billing_postcode - billing.country = payment.billing_country_code - billing.state = payment.billing_country_area + billing.firstName = _billing_address["first_name"] + billing.lastName = _billing_address["last_name"] + billing.street1 = _billing_address["address_1"] + billing.street2 = _billing_address["address_2"] + billing.city = _billing_address["city"] + billing.postalCode = _billing_address["postcode"] + billing.country = _billing_address["country_code"] + billing.state = _billing_address["country_area"] billing.email = payment.billing_email billing.ipAddress = payment.customer_ip_address return billing diff --git a/payments/cybersource/test_cybersource.py b/payments/cybersource/test_cybersource.py index 9ff21d532..3264b9ad4 100644 --- a/payments/cybersource/test_cybersource.py +++ b/payments/cybersource/test_cybersource.py @@ -2,12 +2,17 @@ from decimal import Decimal from unittest import TestCase from django.core import signing -from mock import patch, MagicMock, Mock +try: + from unittest.mock import patch, MagicMock +except ImportError: + from mock import patch, MagicMock from . import CyberSourceProvider, AUTHENTICATE_REQUIRED, ACCEPTED, \ TRANSACTION_SETTLED from .. import PaymentStatus, PurchasedItem, RedirectNeeded +from ..testcommon import create_test_payment + MERCHANT_ID = 'abcd1234' PASSWORD = '1234abdd1234abcd' ORG_ID = 'abc' @@ -20,40 +25,13 @@ 'cvv2': '1234', 'fingerprint': 'abcd1234'} - -class Payment(Mock): - id = 1 - variant = 'cybersource' - currency = 'USD' - total = 100 - status = PaymentStatus.WAITING - transaction_id = None - captured_amount = 0 - message = '' - +_Payment = create_test_payment() +class Payment(_Payment): + # MagicMock is not serializable so overwrite attrs Proxy class attrs(object): fingerprint_session_id = 'fake' merchant_defined_data = {} - - def get_process_url(self): - return 'http://example.com' - - def get_failure_url(self): - return 'http://cancel.com' - - def get_success_url(self): - return 'http://success.com' - - def change_status(self, status, message=''): - self.status = status - self.message = message - - def get_purchased_items(self): - return [ - PurchasedItem( - name='foo', quantity=Decimal('10'), price=Decimal('20'), - currency='USD', sku='bar')] - + capture = {} class TestCybersourceProvider(TestCase): diff --git a/payments/dotpay/test_dotpay.py b/payments/dotpay/test_dotpay.py index 73d4b44e2..b70dc3323 100644 --- a/payments/dotpay/test_dotpay.py +++ b/payments/dotpay/test_dotpay.py @@ -3,11 +3,15 @@ from unittest import TestCase from django.http import HttpResponse, HttpResponseForbidden -from mock import MagicMock, Mock +try: + from unittest.mock import MagicMock +except ImportError: + from mock import MagicMock from .. import PaymentStatus from .forms import ACCEPTED, REJECTED from . import DotpayProvider +from ..testcommon import create_test_payment VARIANT = 'dotpay' PIN = '123' @@ -45,24 +49,7 @@ def get_post_with_md5(post): return post -class Payment(Mock): - id = 1 - variant = VARIANT - currency = 'USD' - total = 100 - status = PaymentStatus.WAITING - - def get_process_url(self): - return 'http://example.com' - - def get_failure_url(self): - return 'http://cancel.com' - - def get_success_url(self): - return 'http://success.com' - - def change_status(self, status): - self.status = status +Payment = create_test_payment(variant=VARIANT, id=1, currency='USD') class TestDotpayProvider(TestCase): diff --git a/payments/dummy/test_dummy.py b/payments/dummy/test_dummy.py index e61e6262a..3f6e8649e 100644 --- a/payments/dummy/test_dummy.py +++ b/payments/dummy/test_dummy.py @@ -12,32 +12,12 @@ from . import DummyProvider from .. import FraudStatus, PaymentError, PaymentStatus, RedirectNeeded +from ..testcommon import create_test_payment VARIANT = 'dummy-3ds' -class Payment(object): - id = 1 - variant = VARIANT - currency = 'USD' - total = 100 - status = PaymentStatus.WAITING - fraud_status = '' - - def get_process_url(self): - return 'http://example.com' - - def get_failure_url(self): - return 'http://cancel.com' - - def get_success_url(self): - return 'http://success.com' - - def change_status(self, new_status): - self.status = new_status - - def change_fraud_status(self, fraud_status): - self.fraud_status = fraud_status +Payment = create_test_payment(variant=VARIANT) class TestDummy3DSProvider(TestCase): diff --git a/payments/models.py b/payments/models.py index f4266176f..77ebdb211 100644 --- a/payments/models.py +++ b/payments/models.py @@ -184,3 +184,6 @@ class BasePaymentWithAddress(BasePayment): """ Has real billing address + shippingaddress alias on billing address (alias for backward compatibility) """ get_billing_address = getter_prefixed_address("billing") get_shipping_address = get_billing_address + + class Meta: + abstract = True diff --git a/payments/paypal/test_paypal.py b/payments/paypal/test_paypal.py index d2ac278f8..441c51dcc 100644 --- a/payments/paypal/test_paypal.py +++ b/payments/paypal/test_paypal.py @@ -2,18 +2,22 @@ import json from decimal import Decimal from unittest import TestCase -from mock import patch, MagicMock, Mock +try: + from unittest.mock import patch, MagicMock +except ImportError: + from mock import patch, MagicMock from django.utils import timezone from requests import HTTPError from . import PaypalProvider, PaypalCardProvider -from .. import PurchasedItem, RedirectNeeded, PaymentError, PaymentStatus +from .. import RedirectNeeded, PaymentError, PaymentStatus +from ..testcommon import create_test_payment CLIENT_ID = 'abc123' PAYMENT_TOKEN = '5a4dae68-2715-4b1e-8bb2-2c2dbe9255f6' SECRET = '123abc' -VARIANT = 'wallet' +VARIANT = 'paypal' PROCESS_DATA = { 'name': 'John Doe', @@ -22,46 +26,14 @@ 'expiration_1': '2020', 'cvv2': '1234'} - -class Payment(Mock): - id = 1 - description = 'payment' - currency = 'USD' - delivery = Decimal(10) - status = PaymentStatus.WAITING - tax = Decimal(10) - token = PAYMENT_TOKEN - total = Decimal(100) - captured_amount = Decimal(0) - variant = VARIANT - transaction_id = None - message = '' - extra_data = json.dumps({'links': { +Payment = create_test_payment(variant=VARIANT, token=PAYMENT_TOKEN) +Payment.extra_data = json.dumps({'links': { 'approval_url': None, 'capture': {'href': 'http://capture.com'}, 'refund': {'href': 'http://refund.com'}, 'execute': {'href': 'http://execute.com'} }}) - def change_status(self, status, message=''): - self.status = status - self.message = message - - def get_failure_url(self): - return 'http://cancel.com' - - def get_process_url(self): - return 'http://example.com' - - def get_purchased_items(self): - return [ - PurchasedItem( - name='foo', quantity=Decimal('10'), price=Decimal('20'), - currency='USD', sku='bar')] - - def get_success_url(self): - return 'http://success.com' - class TestPaypalProvider(TestCase): diff --git a/payments/sagepay/__init__.py b/payments/sagepay/__init__.py index d2dcc1ca3..d8f9a90ea 100644 --- a/payments/sagepay/__init__.py +++ b/payments/sagepay/__init__.py @@ -60,6 +60,8 @@ def aes_dec(self, data): def get_hidden_fields(self, payment): payment.save() return_url = self.get_return_url(payment) + _billing_address = payment.get_billing_address() + _shipping_address = payment.get_billing_address() data = { 'VendorTxCode': payment.pk, 'Amount': "%.2f" % (payment.total,), @@ -67,23 +69,24 @@ def get_hidden_fields(self, payment): 'Description': "Payment #%s" % (payment.pk,), 'SuccessURL': return_url, 'FailureURL': return_url, - 'BillingSurname': payment.billing_last_name, - 'BillingFirstnames': payment.billing_first_name, - 'BillingAddress1': payment.billing_address_1, - 'BillingAddress2': payment.billing_address_2, - 'BillingCity': payment.billing_city, - 'BillingPostCode': payment.billing_postcode, - 'BillingCountry': payment.billing_country_code, - 'DeliverySurname': payment.billing_last_name, - 'DeliveryFirstnames': payment.billing_first_name, - 'DeliveryAddress1': payment.billing_address_1, - 'DeliveryAddress2': payment.billing_address_2, - 'DeliveryCity': payment.billing_city, - 'DeliveryPostCode': payment.billing_postcode, - 'DeliveryCountry': payment.billing_country_code} - if payment.billing_country_code == 'US': - data['BillingState'] = payment.billing_country_area - data['DeliveryState'] = payment.billing_country_area + 'BillingSurname': _billing_address["last_name"], + 'BillingFirstnames': _billing_address["first_name"], + 'BillingAddress1': _billing_address["address_1"], + 'BillingAddress2': _billing_address["address_2"], + 'BillingCity': _billing_address["city"], + 'BillingPostCode': _billing_address["postcode"], + 'BillingCountry': _billing_address["country_code"], + 'DeliverySurname': _shipping_address["last_name"], + 'DeliveryFirstnames': _shipping_address["first_name"], + 'DeliveryAddress1': _shipping_address["address_1"], + 'DeliveryAddress2': _shipping_address["address_2"], + 'DeliveryCity': _shipping_address["city"], + 'DeliveryPostCode': _shipping_address["postcode"], + 'DeliveryCountry': _shipping_address["country_code"]} + if _billing_address["country_code"] == 'US': + data['BillingState'] = _billing_address["country_area"] + if _shipping_address["country_code"] == 'US': + data['DeliveryState'] = _shipping_address["country_area"] udata = "&".join("%s=%s" % kv for kv in data.items()) crypt = self.aes_enc(udata) return {'VPSProtocol': self._version, 'TxType': 'PAYMENT', diff --git a/payments/sagepay/test_sagepay.py b/payments/sagepay/test_sagepay.py index 5686dfa70..6cc96722e 100644 --- a/payments/sagepay/test_sagepay.py +++ b/payments/sagepay/test_sagepay.py @@ -1,36 +1,20 @@ from __future__ import unicode_literals from unittest import TestCase -from mock import patch, MagicMock, Mock +try: + from unittest.mock import patch, MagicMock +except ImportError: + from mock import patch, MagicMock from . import SagepayProvider from .. import PaymentStatus +from ..testcommon import create_test_payment VENDOR = 'abcd1234' ENCRYPTION_KEY = '1234abdd1234abcd' -class Payment(Mock): - id = 1 - variant = 'sagepay' - currency = 'USD' - total = 100 - status = PaymentStatus.WAITING - transaction_id = None - captured_amount = 0 - billing_first_name = 'John' - - def get_process_url(self): - return 'http://example.com' - - def get_failure_url(self): - return 'http://cancel.com' - - def get_success_url(self): - return 'http://success.com' - - def change_status(self, status): - self.status = status +Payment = create_test_payment() class TestSagepayProvider(TestCase): diff --git a/payments/sofort/__init__.py b/payments/sofort/__init__.py index 4035225c0..9d0dacee3 100644 --- a/payments/sofort/__init__.py +++ b/payments/sofort/__init__.py @@ -12,7 +12,7 @@ class SofortProvider(BasicProvider): - + def __init__(self, *args, **kwargs): self.secret = kwargs.pop('key') self.client_id = kwargs.pop('id') @@ -20,7 +20,7 @@ def __init__(self, *args, **kwargs): self.endpoint = kwargs.pop( 'endpoint', 'https://api.sofort.com/api/xml') super(SofortProvider, self).__init__(*args, **kwargs) - + def post_request(self, xml_request): response = requests.post( self.endpoint, @@ -29,7 +29,7 @@ def post_request(self, xml_request): auth=(self.client_id, self.secret)) doc = xmltodict.parse(response.content) return doc, response - + def get_form(self, payment, data=None): if not payment.id: payment.save() @@ -75,12 +75,13 @@ def process_data(self, payment, request): payment.captured_amount = payment.total payment.change_status(PaymentStatus.CONFIRMED) payment.extra_data = json.dumps(doc) - sender_data = doc['transactions']['transaction_details']['sender'] - holder_data = sender_data['holder'] - first_name, last_name = holder_data.rsplit(' ', 1) - payment.billing_first_name = first_name - payment.billing_last_name = last_name - payment.billing_country_code = sender_data['country_code'] + # overwriting names should not be possible + #sender_data = doc['transactions']['transaction_details']['sender'] + #holder_data = sender_data['holder'] + #first_name, last_name = holder_data.rsplit(' ', 1) + #payment.billing_first_name = first_name + #payment.billing_last_name = last_name + #payment.billing_country_code = sender_data['country_code'] payment.save() return redirect(payment.get_success_url()) diff --git a/payments/sofort/test_sofort.py b/payments/sofort/test_sofort.py index 4096cf577..1fdda67d8 100644 --- a/payments/sofort/test_sofort.py +++ b/payments/sofort/test_sofort.py @@ -1,37 +1,21 @@ from __future__ import unicode_literals -from unittest import TestCase -from mock import patch, MagicMock, Mock import json +from unittest import TestCase +try: + from unittest.mock import patch, MagicMock +except ImportError: + from mock import patch, MagicMock from . import SofortProvider from .. import PaymentStatus, RedirectNeeded +from ..testcommon import create_test_payment SECRET = 'abcd1234' CLIENT_ID = '1234' PROJECT_ID = 'abcd' -class Payment(Mock): - id = 1 - variant = 'sagepay' - currency = 'USD' - total = 100 - status = PaymentStatus.WAITING - transaction_id = None - captured_amount = 0 - billing_first_name = 'John' - - def get_process_url(self): - return 'http://example.com' - - def get_failure_url(self): - return 'http://cancel.com' - - def get_success_url(self): - return 'http://success.com' - - def change_status(self, status): - self.status = status +Payment = create_test_payment() class TestSofortProvider(TestCase): diff --git a/payments/stripe/forms.py b/payments/stripe/forms.py index 0c83657be..32d622759 100644 --- a/payments/stripe/forms.py +++ b/payments/stripe/forms.py @@ -29,6 +29,7 @@ def clean(self): if not self.errors: if not self.payment.transaction_id: stripe.api_key = self.provider.secret_key + _billing_address = self.payment.get_billing_address() try: self.charge = stripe.Charge.create( capture=False, @@ -36,8 +37,8 @@ def clean(self): currency=self.payment.currency, card=data['stripeToken'], description='%s %s' % ( - self.payment.billing_last_name, - self.payment.billing_first_name)) + _billing_address["last_name"], + _billing_address["first_name"])) except stripe.CardError as e: # Making sure we retrieve the charge charge_id = e.json_body['error']['charge'] @@ -81,14 +82,15 @@ class PaymentForm(StripeFormMixin, CreditCardPaymentFormWithName): def __init__(self, *args, **kwargs): super(PaymentForm, self).__init__(*args, **kwargs) + _billing_address = self.payment.get_billing_address() stripe_attrs = self.fields['stripeToken'].widget.attrs stripe_attrs['data-publishable-key'] = self.provider.public_key - stripe_attrs['data-address-line1'] = self.payment.billing_address_1 - stripe_attrs['data-address-line2'] = self.payment.billing_address_2 - stripe_attrs['data-address-city'] = self.payment.billing_city - stripe_attrs['data-address-state'] = self.payment.billing_country_area - stripe_attrs['data-address-zip'] = self.payment.billing_postcode - stripe_attrs['data-address-country'] = self.payment.billing_country_code + stripe_attrs['data-address-line1'] = _billing_address["address_1"] + stripe_attrs['data-address-line2'] = _billing_address["address_2"] + stripe_attrs['data-address-city'] = _billing_address["city"] + stripe_attrs['data-address-state'] = _billing_address["country_area"] + stripe_attrs['data-address-zip'] = _billing_address["postcode"] + stripe_attrs['data-address-country'] = _billing_address["country_code"] widget_map = { 'name': SensitiveTextInput( attrs={'autocomplete': 'cc-name', 'required': 'required'}), diff --git a/payments/stripe/test_stripe.py b/payments/stripe/test_stripe.py index dd08ccfde..eca8f2058 100644 --- a/payments/stripe/test_stripe.py +++ b/payments/stripe/test_stripe.py @@ -2,56 +2,23 @@ from __future__ import unicode_literals from contextlib import contextmanager -from mock import patch, Mock from unittest import TestCase +try: + from unittest.mock import patch +except ImportError: + from mock import patch import stripe from . import StripeProvider, StripeCardProvider from .. import FraudStatus, PaymentStatus, RedirectNeeded +from ..testcommon import create_test_payment SECRET_KEY = '1234abcd' PUBLIC_KEY = 'abcd1234' -class Payment(Mock): - - id = 1 - description = 'payment' - currency = 'USD' - delivery = 10 - status = PaymentStatus.WAITING - message = None - tax = 10 - total = 100 - captured_amount = 0 - transaction_id = None - - def change_status(self, status, message=''): - self.status = status - self.message = message - - def change_fraud_status(self, status, message='', commit=True): - self.fraud_status = status - self.fraud_message = message - - def capture(self, amount=None): - amount = amount or self.total - self.captured_amount = amount - self.change_status(PaymentStatus.CONFIRMED) - - def get_failure_url(self): - return 'http://cancel.com' - - def get_process_url(self): - return 'http://example.com' - - def get_purchased_items(self): - return [] - - def get_success_url(self): - return 'http://success.com' - +Payment = create_test_payment() @contextmanager def mock_stripe_Charge_create(error_msg=None): diff --git a/payments/test_core.py b/payments/test_core.py index f6f3001fe..a4d918ca7 100644 --- a/payments/test_core.py +++ b/payments/test_core.py @@ -1,7 +1,10 @@ from __future__ import unicode_literals from decimal import Decimal from unittest import TestCase -from mock import patch, NonCallableMock +try: + from unittest.mock import patch, NonCallableMock +except ImportError: + from mock import patch, NonCallableMock from payments import core from .forms import CreditCardPaymentFormWithName, PaymentForm diff --git a/payments/testcommon.py b/payments/testcommon.py index 37c9e75d4..de568ceaa 100644 --- a/payments/testcommon.py +++ b/payments/testcommon.py @@ -4,11 +4,13 @@ from . import PaymentStatus, PurchasedItem from .utils import getter_prefixed_address from datetime import datetime +try: + from unittest.mock import Mock +except ImportError: + from mock import Mock def create_test_payment(**_kwargs): - class TestPayment(BasePaymentLogic): - def __init__(self, **kwargs): - self.__dict__.update(kwargs) + class TestPayment(Mock, BasePaymentLogic): id = 523 pk = id description = 'payment' @@ -17,12 +19,12 @@ def __init__(self, **kwargs): status = PaymentStatus.WAITING message = "" tax = Decimal(10) - token = "undefined" + token = "354338723" total = Decimal(100) captured_amount = Decimal("0.0") extra_data = "" - variant = "342" - transaction_id = "" + variant = "undefined" + transaction_id = None created = datetime.now() modified = datetime.now() @@ -35,9 +37,23 @@ def __init__(self, **kwargs): billing_country_code = "US" billing_country_area = "Tennessee" billing_email = "example@example.com" - customer_ip_address = "192.78.6.6" + get_billing_address = getter_prefixed_address("billing") + get_shipping_address = get_billing_address + + def capture(self, amount=None): + amount = amount or self.total + self.captured_amount = amount + self.change_status(PaymentStatus.CONFIRMED) + + def change_status(self, status, message=''): + ''' + Updates the Payment status and sends the status_changed signal. + ''' + self.status = status + self.message = message + def get_purchased_items(self): return [ PurchasedItem( @@ -55,5 +71,7 @@ def get_success_url(self): def save(self): return self - TestPayment.__dict__.update(_kwargs) + # workaround limitation in python + for key, val in _kwargs.items(): + setattr(TestPayment, key, val) return TestPayment diff --git a/payments/wallet/test_wallet.py b/payments/wallet/test_wallet.py index 5a984a634..d5f9c394b 100644 --- a/payments/wallet/test_wallet.py +++ b/payments/wallet/test_wallet.py @@ -1,14 +1,18 @@ from __future__ import unicode_literals import time from decimal import Decimal -from unittest import TestCase from django.http import HttpResponse, HttpResponseForbidden import jwt -from mock import MagicMock +from unittest import TestCase +try: + from unittest.mock import MagicMock +except ImportError: + from mock import MagicMock from .. import PaymentStatus from . import GoogleWalletProvider +from ..testcommon import create_test_payment PAYMENT_TOKEN = '5a4dae68-2715-4b1e-8bb2-2c2dbe9255f6' SELLER_ID = 'abc123' @@ -31,35 +35,7 @@ 'orderId': '1234567890'}} -class Payment(object): - - id = 1 - description = 'payment' - currency = 'USD' - delivery = Decimal(10) - status = PaymentStatus.WAITING - tax = Decimal(10) - token = PAYMENT_TOKEN - total = Decimal(100) - variant = VARIANT - - def change_status(self, status): - self.status = status - - def get_failure_url(self): - return 'http://cancel.com' - - def get_process_url(self): - return 'http://example.com' - - def get_purchased_items(self): - return [] - - def save(self): - return self - - def get_success_url(self): - return 'http://success.com' +Payment = create_test_payment(variant=VARIANT, token=PAYMENT_TOKEN) class TestGoogleWalletProvider(TestCase): diff --git a/setup.py b/setup.py index 520b268d5..6f811f6c2 100755 --- a/setup.py +++ b/setup.py @@ -30,11 +30,17 @@ 'suds-jurko>=0.6', 'xmltodict>=0.9.2'] +TEST_REQUIREMENTS = [ + 'pytest', + 'pytest-django' +] +if sys.version_info.major < 3: + TEST_REQUIREMENTS.append('mock') class PyTest(TestCommand): user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")] test_args = [] - + def initialize_options(self): TestCommand.initialize_options(self) self.pytest_args = [] @@ -76,8 +82,5 @@ def run_tests(self): install_requires=REQUIREMENTS, cmdclass={ 'test': PyTest}, - tests_require=[ - 'mock', - 'pytest', - 'pytest-django'], + tests_require=TEST_REQUIREMENTS, zip_safe=False) From 52fb42390e3520a40f812da3bde490d0c0525bce Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 8 Oct 2017 13:33:38 +0200 Subject: [PATCH 3/7] fix tests and missing simplejson dependency --- payments/testcommon.py | 2 +- setup.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/payments/testcommon.py b/payments/testcommon.py index de568ceaa..cf0ada0d9 100644 --- a/payments/testcommon.py +++ b/payments/testcommon.py @@ -57,7 +57,7 @@ def change_status(self, status, message=''): def get_purchased_items(self): return [ PurchasedItem( - name='foo', quantity=Decimal('10'), price=Decimal('20'), + name='foo', quantity=10, price=Decimal('20'), currency='USD', sku='bar')] def get_failure_url(self): diff --git a/setup.py b/setup.py index 6f811f6c2..61e5345d0 100755 --- a/setup.py +++ b/setup.py @@ -28,7 +28,8 @@ 'requests>=1.2.0', 'stripe>=1.9.8', 'suds-jurko>=0.6', - 'xmltodict>=0.9.2'] + 'xmltodict>=0.9.2' + 'simplejson>=3.11'] TEST_REQUIREMENTS = [ 'pytest', From 736e16a47d346178c720c0a9b6c7b5d163f45cea Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 8 Oct 2017 20:33:21 +0200 Subject: [PATCH 4/7] simplejson only required from paydirekt, and a comma is missing --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 61e5345d0..6f811f6c2 100755 --- a/setup.py +++ b/setup.py @@ -28,8 +28,7 @@ 'requests>=1.2.0', 'stripe>=1.9.8', 'suds-jurko>=0.6', - 'xmltodict>=0.9.2' - 'simplejson>=3.11'] + 'xmltodict>=0.9.2'] TEST_REQUIREMENTS = [ 'pytest', From cee3967527337920d74c9fefa37425841919efab Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 8 Oct 2017 23:22:58 +0200 Subject: [PATCH 5/7] speed up extract_streetnr, fix possible regex attack --- payments/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/utils.py b/payments/utils.py index d14de2dcd..210d550b3 100644 --- a/payments/utils.py +++ b/payments/utils.py @@ -17,7 +17,7 @@ def get_year_choices(): _extract_streetnr = re.compile(r"([0-9]+)\s*$") def extract_streetnr(address, fallback=None): - ret = _extract_streetnr.findall(address) + ret = _extract_streetnr.findall(address[-15:]) if ret: return ret[0] else: From fb0a31ef5f44e762ea666e86154a7f5fce29a3ed Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 9 Oct 2017 23:09:42 +0200 Subject: [PATCH 6/7] fix string instead Decimal bug for empty values --- payments/models.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/payments/models.py b/payments/models.py index 77ebdb211..dc4d1e7bb 100644 --- a/payments/models.py +++ b/payments/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import json from uuid import uuid4 +from decimal import Decimal from django.conf import settings from django.core.urlresolvers import reverse @@ -159,10 +160,10 @@ class BasePayment(models.Model, BasePaymentLogic): #: Currency code (may be provider-specific) currency = models.CharField(max_length=10) #: Total amount (gross) - total = models.DecimalField(max_digits=9, decimal_places=2, default='0.0') + total = models.DecimalField(max_digits=9, decimal_places=2, default=Decimal('0.0')) delivery = models.DecimalField( - max_digits=9, decimal_places=2, default='0.0') - tax = models.DecimalField(max_digits=9, decimal_places=2, default='0.0') + max_digits=9, decimal_places=2, default=Decimal('0.0')) + tax = models.DecimalField(max_digits=9, decimal_places=2, default=Decimal('0.0')) description = models.TextField(blank=True, default='') billing_email = models.EmailField(blank=True) customer_ip_address = models.GenericIPAddressField(blank=True, null=True) @@ -170,7 +171,7 @@ class BasePayment(models.Model, BasePaymentLogic): message = models.TextField(blank=True, default='') token = models.CharField(max_length=36, blank=True, default='') captured_amount = models.DecimalField( - max_digits=9, decimal_places=2, default='0.0') + max_digits=9, decimal_places=2, default=Decimal('0.0')) class Meta: abstract = True From 0ec58160b3e61b46ba160464419e3fd9fbce05b2 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 12 Oct 2017 18:59:18 +0200 Subject: [PATCH 7/7] replace extract_streetnr with split_streetnr --- payments/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/payments/utils.py b/payments/utils.py index 210d550b3..d2b7eed02 100644 --- a/payments/utils.py +++ b/payments/utils.py @@ -16,12 +16,12 @@ def get_year_choices(): return [('', _('Year'))] + year_choices _extract_streetnr = re.compile(r"([0-9]+)\s*$") -def extract_streetnr(address, fallback=None): - ret = _extract_streetnr.findall(address[-15:]) +def split_streetnr(address, fallback=None): + ret = _extract_streetnr.search(address[-15:]) if ret: - return ret[0] + return address[:(ret.start()-15)].strip(), ret.group(0) else: - return fallback + return address.strip(), fallback def getter_prefixed_address(prefix): """ create getter for prefixed address format """