From 8303900c5f1c7d5960bdd7a1a5be3f2e1f770aad Mon Sep 17 00:00:00 2001 From: NyanKiyoshi <6186720+NyanKiyoshi@users.noreply.github.com> Date: Fri, 10 Aug 2018 10:39:21 +0200 Subject: [PATCH 1/7] Moved base implementation to its own package --- django_payments_razorpay/__init__.py | 59 +++++++++++++++++++++++++++ django_payments_razorpay/forms.py | 50 +++++++++++++++++++++++ django_payments_razorpay/widgets.py | 44 ++++++++++++++++++++ setup.py | 60 ++++++++++++++++++++++++++++ 4 files changed, 213 insertions(+) create mode 100644 django_payments_razorpay/__init__.py create mode 100644 django_payments_razorpay/forms.py create mode 100644 django_payments_razorpay/widgets.py create mode 100755 setup.py diff --git a/django_payments_razorpay/__init__.py b/django_payments_razorpay/__init__.py new file mode 100644 index 0000000..1d4138e --- /dev/null +++ b/django_payments_razorpay/__init__.py @@ -0,0 +1,59 @@ +import json +from decimal import Decimal + +from payments import PaymentStatus, RedirectNeeded +from payments.core import BasicProvider +from .forms import ModalPaymentForm +import razorpay +import razorpay.errors + + +class RazorPayProvider(BasicProvider): + + form_class = ModalPaymentForm + ACCEPTED_CURRENCIES = 'INR', + + def __init__( + self, + public_key, secret_key, + image='', name='', prefill=False, **kwargs): + + # TODO: warn on docs: paisa is the only support currency as of now + self.secret_key = secret_key + self.public_key = public_key + self.image = image + self.name = name + self.prefill = prefill + self.razorpay_client = razorpay.Client(auth=(public_key, secret_key)) + + super(RazorPayProvider, self).__init__(**kwargs) + + def get_form(self, payment, data=None): + # TODO: raise error if payment.currency is not in ACCEPTED_CURRENCIES + + if payment.status == PaymentStatus.WAITING: + payment.change_status(PaymentStatus.INPUT) + + form = self.form_class( + data=data, payment=payment, provider=self) + + if form.is_valid(): + form.save() + raise RedirectNeeded(payment.get_success_url()) + return form + + def charge(self, transaction_id, payment): + amount = int(payment.total * 100) + charge = self.razorpay_client.payment.capture(transaction_id, amount) + return charge + + def refund(self, payment, amount=None): + amount = int((amount or payment.captured_amount) * 100) + try: + refund = self.razorpay_client.payment.refund( + payment.transaction_id, amount) + except razorpay.errors.BadRequestError as exc: + raise ValueError(str(exc)) + refunded_amount = Decimal(refund['amount']) / 100 + payment.attrs.refund = json.dumps(refund) + return refunded_amount diff --git a/django_payments_razorpay/forms.py b/django_payments_razorpay/forms.py new file mode 100644 index 0000000..b0c7e7f --- /dev/null +++ b/django_payments_razorpay/forms.py @@ -0,0 +1,50 @@ +import json +from decimal import Decimal + +from django import forms +from django.utils.translation import ugettext as _ +from payments import PaymentStatus +from payments.forms import PaymentForm + +from .widgets import RazorPayCheckoutWidget + + +class ModalPaymentForm(PaymentForm): + razorpay_payment_id = forms.CharField( + required=True, widget=forms.HiddenInput) + + def __init__(self, *args, **kwargs): + super(ModalPaymentForm, self).__init__( + hidden_inputs=False, autosubmit=True, *args, **kwargs) + + widget = RazorPayCheckoutWidget( + provider=self.provider, payment=self.payment) + self.fields['razorpay'] = forms.CharField( + widget=widget, required=False) + self.transaction_id = None + + # TODO: add note to the docs saying there is no fraud status + def clean(self): + data = super(ModalPaymentForm, self).clean() + + if self.payment.transaction_id: + msg = _('This payment has already been processed.') + self._errors['__all__'] = self.error_class([msg]) + else: + self.transaction_id = data['razorpay_payment_id'] + + charge = self.provider.charge(self.transaction_id, self.payment) + captured_amount = Decimal(charge['amount']) / 100 + + # FIXME: should we handle the case + # of having the captured amount invalid? + self.payment.attrs.capture = json.dumps(charge) + self.payment.captured_amount = captured_amount + + assert captured_amount == self.payment.total + + return data + + def save(self): + self.payment.transaction_id = self.transaction_id + self.payment.change_status(PaymentStatus.CONFIRMED) diff --git a/django_payments_razorpay/widgets.py b/django_payments_razorpay/widgets.py new file mode 100644 index 0000000..6dd621e --- /dev/null +++ b/django_payments_razorpay/widgets.py @@ -0,0 +1,44 @@ +from django.forms.widgets import HiddenInput +from django.utils.html import format_html +from django.utils.translation import ugettext_lazy as _ + +try: + from django.forms.utils import flatatt +except ImportError: + from django.forms.util import flatatt + +CHECKOUT_SCRIPT_URL = 'https://checkout.razorpay.com/v1/checkout.js' + + +# TODO: add note to docs: you can use any valid card number +# like 4111 1111 1111 1111 with any future expiry date and CVV in the test mode +class RazorPayCheckoutWidget(HiddenInput): + def __init__(self, provider, payment, *args, **kwargs): + override_attrs = kwargs.get('attrs', None) + base_attrs = kwargs['attrs'] = { + 'src': CHECKOUT_SCRIPT_URL, + 'data-key': provider.public_key, + 'data-buttontext': _('Pay now with Razorpay'), + 'data-image': provider.image, + 'data-name': provider.name, + 'data-description': payment.description or _('Total payment'), + 'data-amount': int(payment.total * 100) + } + + if provider.prefill: + customer_name = '%s %s' % ( + payment.billing_last_name, + payment.billing_first_name) + base_attrs.update({ + 'data-prefill.name': customer_name, + 'data-prefill.email': payment.billing_email + }) + + if override_attrs: + base_attrs.update(override_attrs) + super(RazorPayCheckoutWidget, self).__init__(*args, **kwargs) + + def render(self, name, *args, **kwargs): + attrs = kwargs.setdefault('attrs', {}) + attrs.update(self.attrs) + return format_html('', flatatt(attrs)) diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..b9a74ce --- /dev/null +++ b/setup.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +from setuptools import setup +from setuptools.command.test import test as TestCommand +from sys import version_info, exit + + +REQUIREMENTS = ['django-payments>=0.12.3', 'razorpay>=1.1.1'] +TEST_REQUIREMENTS = ['pytest', 'pytest-django'] + +if version_info < (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 = [] + + def finalize_options(self): + TestCommand.finalize_options(self) + self.test_args = [] + self.test_suite = True + + def run_tests(self): + # import here, cause outside the eggs aren't loaded + import pytest + errno = pytest.main(self.pytest_args) + exit(errno) + + +setup( + name='django-payments-razorpay', + author='NyanKiyoshi', + author_email='hello@vanille.bid', + url='https://github.com/NyanKiyoshi/django-payments-razorpay', + description='Razorpay provider for django-payments.', + version='0.0.0', + packages=['django_payments_razorpay'], + include_package_data=True, + classifiers=[ + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Framework :: Django', + 'Topic :: Software Development :: Libraries :: Application Frameworks', + 'Topic :: Software Development :: Libraries :: Python Modules'], + install_requires=REQUIREMENTS, + cmdclass={'test': PyTest}, + tests_require=TEST_REQUIREMENTS, + zip_safe=False) From 620c6e37aa8b60d6ba65f058c5c891bd66cf538a Mon Sep 17 00:00:00 2001 From: NyanKiyoshi <6186720+NyanKiyoshi@users.noreply.github.com> Date: Fri, 10 Aug 2018 11:14:24 +0200 Subject: [PATCH 2/7] Added documentation --- README.md | 48 ++++++++++++++++++++++++++++ django_payments_razorpay/__init__.py | 1 - django_payments_razorpay/forms.py | 1 - django_payments_razorpay/widgets.py | 2 -- 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..72ce528 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# Razorpay for django-payments + +**WARNING:** only the paisa (INR) currency is supported by Razorpay as of now. + +## Installation +Add `django-payments-razorpay` to your project requirements +and/ or run the installation with: +```shell +pip install django-payments-razorpay +``` + + +## Provider parameters +First of all, to create your API credentials, you need to go in your Razorpay account settings, +then in the API Keys section ([direct link](https://dashboard.razorpay.com/#/app/keys)). + +| Key | Required | Type | Description | +| ------------ | ------- | --------- | ----------- | +| `public_key` | Yes | `string` | Your Razorpay **key id** | +| `secret_key` | Yes | `string` | Your Razorpay **secret key id** | +| `image` | No | `string` | An absolute or relative link to your store logo | +| `name` | No | `string` | Your store name | +| `prefill` | No | `boolean` | Pre-fill the email and customer's full name if set to `True` (disabled by default) | + + +## Example configuration + +In your `settings.py` file, you can add the following keys or append the data to them: + +```python +PAYMENT_VARIANTS = { + 'razorpay': ('django_payments_razorpay.RazorPayProvider', { + 'public_key': 'RAZORPAY_PUBLIC_KEY', + 'secret_key': 'RAZORPAY_SECRET_KEY'})} +``` + +Note: if you are using **Saleor**, you may want to add Razorpay to the checkout payment choices: + +```python +CHECKOUT_PAYMENT_CHOICES = [ + ('razorpay', 'RazorPay')] +``` + + +## Notes +1. Razorpay automatically capture the whole payment amount; +2. In test mode, you can use `4111 1111 1111 1111` (or any other valid credit card numbers) +with any future expiry date and CVV to pay orders. diff --git a/django_payments_razorpay/__init__.py b/django_payments_razorpay/__init__.py index 1d4138e..ccae59b 100644 --- a/django_payments_razorpay/__init__.py +++ b/django_payments_razorpay/__init__.py @@ -18,7 +18,6 @@ def __init__( public_key, secret_key, image='', name='', prefill=False, **kwargs): - # TODO: warn on docs: paisa is the only support currency as of now self.secret_key = secret_key self.public_key = public_key self.image = image diff --git a/django_payments_razorpay/forms.py b/django_payments_razorpay/forms.py index b0c7e7f..0b7b30d 100644 --- a/django_payments_razorpay/forms.py +++ b/django_payments_razorpay/forms.py @@ -23,7 +23,6 @@ def __init__(self, *args, **kwargs): widget=widget, required=False) self.transaction_id = None - # TODO: add note to the docs saying there is no fraud status def clean(self): data = super(ModalPaymentForm, self).clean() diff --git a/django_payments_razorpay/widgets.py b/django_payments_razorpay/widgets.py index 6dd621e..bb5583f 100644 --- a/django_payments_razorpay/widgets.py +++ b/django_payments_razorpay/widgets.py @@ -10,8 +10,6 @@ CHECKOUT_SCRIPT_URL = 'https://checkout.razorpay.com/v1/checkout.js' -# TODO: add note to docs: you can use any valid card number -# like 4111 1111 1111 1111 with any future expiry date and CVV in the test mode class RazorPayCheckoutWidget(HiddenInput): def __init__(self, provider, payment, *args, **kwargs): override_attrs = kwargs.get('attrs', None) From 6aeee77587b27ee9aaa0c2296c1e92ff23ac3d7b Mon Sep 17 00:00:00 2001 From: NyanKiyoshi <6186720+NyanKiyoshi@users.noreply.github.com> Date: Fri, 10 Aug 2018 15:38:41 +0200 Subject: [PATCH 3/7] Implemented tests --- .travis.yml | 42 +++++++++++ README.md | 2 +- django_payments_razorpay/__init__.py | 4 - django_payments_razorpay/forms.py | 11 +-- django_payments_razorpay/widgets.py | 5 +- setup.py | 29 +------- tests/__init__.py | 5 ++ tests/conftest.py | 107 +++++++++++++++++++++++++++ tests/settings.py | 15 ++++ tests/test_forms.py | 36 +++++++++ tests/test_provider.py | 91 +++++++++++++++++++++++ tests/test_widgets.py | 37 +++++++++ tox.ini | 29 ++++++++ 13 files changed, 369 insertions(+), 44 deletions(-) create mode 100644 .travis.yml create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/settings.py create mode 100644 tests/test_forms.py create mode 100644 tests/test_provider.py create mode 100644 tests/test_widgets.py create mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..610f2a7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,42 @@ +sudo: false +language: python +python: + - "3.4" + - "3.5" + - "3.6" +env: + - DJANGO="1.11" + - DJANGO="2.0" + - DJANGO="master" +matrix: + include: + - python: "2.7" + env: DJANGO="1.11" + - python: "3.7" + sudo: required + dist: xenial + env: DJANGO="2.0" + - python: "3.7" + sudo: required + dist: xenial + env: DJANGO="2.1" + - python: "3.7" + sudo: required + dist: xenial + env: DJANGO="master" + allow_failures: + - python: "3.4" + env: DJANGO="2.0" + - python: "3.4" + env: DJANGO="master" + - python: "3.5" + env: DJANGO="2.0" + - python: "3.5" + env: DJANGO="master" + - python: "3.6" + env: DJANGO="2.0" + - python: "3.6" + env: DJANGO="master" +after_success: codecov +install: pip install tox-travis codecov +script: tox diff --git a/README.md b/README.md index 72ce528..73eeae6 100644 --- a/README.md +++ b/README.md @@ -45,4 +45,4 @@ CHECKOUT_PAYMENT_CHOICES = [ ## Notes 1. Razorpay automatically capture the whole payment amount; 2. In test mode, you can use `4111 1111 1111 1111` (or any other valid credit card numbers) -with any future expiry date and CVV to pay orders. +with any future expiry date and CVV to pay orders. diff --git a/django_payments_razorpay/__init__.py b/django_payments_razorpay/__init__.py index ccae59b..f574967 100644 --- a/django_payments_razorpay/__init__.py +++ b/django_payments_razorpay/__init__.py @@ -11,7 +11,6 @@ class RazorPayProvider(BasicProvider): form_class = ModalPaymentForm - ACCEPTED_CURRENCIES = 'INR', def __init__( self, @@ -28,8 +27,6 @@ def __init__( super(RazorPayProvider, self).__init__(**kwargs) def get_form(self, payment, data=None): - # TODO: raise error if payment.currency is not in ACCEPTED_CURRENCIES - if payment.status == PaymentStatus.WAITING: payment.change_status(PaymentStatus.INPUT) @@ -37,7 +34,6 @@ def get_form(self, payment, data=None): data=data, payment=payment, provider=self) if form.is_valid(): - form.save() raise RedirectNeeded(payment.get_success_url()) return form diff --git a/django_payments_razorpay/forms.py b/django_payments_razorpay/forms.py index 0b7b30d..59440ec 100644 --- a/django_payments_razorpay/forms.py +++ b/django_payments_razorpay/forms.py @@ -35,15 +35,8 @@ def clean(self): charge = self.provider.charge(self.transaction_id, self.payment) captured_amount = Decimal(charge['amount']) / 100 - # FIXME: should we handle the case - # of having the captured amount invalid? self.payment.attrs.capture = json.dumps(charge) self.payment.captured_amount = captured_amount - - assert captured_amount == self.payment.total - + self.payment.transaction_id = self.transaction_id + self.payment.change_status(PaymentStatus.CONFIRMED) return data - - def save(self): - self.payment.transaction_id = self.transaction_id - self.payment.change_status(PaymentStatus.CONFIRMED) diff --git a/django_payments_razorpay/widgets.py b/django_payments_razorpay/widgets.py index bb5583f..d4bb950 100644 --- a/django_payments_razorpay/widgets.py +++ b/django_payments_razorpay/widgets.py @@ -20,7 +20,8 @@ def __init__(self, provider, payment, *args, **kwargs): 'data-image': provider.image, 'data-name': provider.name, 'data-description': payment.description or _('Total payment'), - 'data-amount': int(payment.total * 100) + 'data-amount': int(payment.total * 100), + 'data-currency': payment.currency } if provider.prefill: @@ -36,7 +37,7 @@ def __init__(self, provider, payment, *args, **kwargs): base_attrs.update(override_attrs) super(RazorPayCheckoutWidget, self).__init__(*args, **kwargs) - def render(self, name, *args, **kwargs): + def render(self, *args, **kwargs): attrs = kwargs.setdefault('attrs', {}) attrs.update(self.attrs) return format_html('', flatatt(attrs)) diff --git a/setup.py b/setup.py index b9a74ce..74b7ece 100755 --- a/setup.py +++ b/setup.py @@ -1,35 +1,9 @@ #!/usr/bin/env python from setuptools import setup -from setuptools.command.test import test as TestCommand -from sys import version_info, exit - REQUIREMENTS = ['django-payments>=0.12.3', 'razorpay>=1.1.1'] -TEST_REQUIREMENTS = ['pytest', 'pytest-django'] - -if version_info < (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 = [] - - def finalize_options(self): - TestCommand.finalize_options(self) - self.test_args = [] - self.test_suite = True - - def run_tests(self): - # import here, cause outside the eggs aren't loaded - import pytest - errno = pytest.main(self.pytest_args) - exit(errno) +TEST_REQUIREMENTS = ['pytest', 'mock'] setup( @@ -55,6 +29,5 @@ def run_tests(self): 'Topic :: Software Development :: Libraries :: Application Frameworks', 'Topic :: Software Development :: Libraries :: Python Modules'], install_requires=REQUIREMENTS, - cmdclass={'test': PyTest}, tests_require=TEST_REQUIREMENTS, zip_safe=False) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..6dc39b6 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,5 @@ +import django +import os + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') +django.setup() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..40fb747 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,107 @@ +from decimal import Decimal + +import pytest +import mock +from payments import PaymentStatus +from payments.models import BasePayment +from django_payments_razorpay import RazorPayProvider + +TRANSACTION_ID = 'pay_7IZD7aJ2kkmOjk' + +CLIENT_ID = 'abc123' +SECRET = '123abc' +PAYMENT_TOKEN = '5a4dae68-2715-4b1e-8bb2-2c2dbe9255f6' +VARIANT = 'razorpay' +EMAIL = 'hello@example.com' +FIRST_NAME = 'John' +LAST_NAME = 'Doe' +TOTAL = Decimal(220) +TOTAL_INT = 22000 + + +@pytest.fixture +def payment(): + payment_data = { + 'description': 'payment', + 'currency': 'USD', + 'delivery': Decimal(10), + 'status': PaymentStatus.WAITING, + 'tax': Decimal(10), + 'token': PAYMENT_TOKEN, + 'total': TOTAL, + 'captured_amount': Decimal(0), + 'variant': VARIANT, + 'transaction_id': None, + 'message': '', + 'billing_first_name': FIRST_NAME, + 'billing_last_name': LAST_NAME, + 'billing_email': EMAIL} + _payment = BasePayment(**payment_data) + _payment.id = 1 + _payment.save = mock.MagicMock() + _payment.get_success_url = mock.MagicMock( + new_callable=lambda: 'https://success') + return _payment + + +@pytest.fixture +def valid_capture_data(): + return { + "id": TRANSACTION_ID, + "entity": "payment", + "amount": TOTAL_INT, + "currency": "INR", + "status": "captured", + "order_id": None, + "invoice_id": None, + "international": False, + "method": "wallet", + "amount_refunded": 0, + "refund_status": None, + "captured": True, + "description": "Purchase Description", + "wallet": "freecharge", + "email": "a@b.com", + "contact": "91xxxxxxxx", + "notes": { + "merchant_order_id": "order id" + }, + "error_code": None, + "error_description": None, + "created_at": 1400826750 + } + + +@pytest.fixture +def valid_full_refund_data(): + return { + "id": "rfnd_5UXHCzSiC02RBz", + "entity": "refund", + "amount": TOTAL_INT, + "currency": "INR", + "payment_id": "pay_5UWttxtCjkrldV", + "notes": {}, + "created_at": 1462887226 + } + + +@pytest.fixture +def valid_partial_refund_data(valid_full_refund_data): + valid_full_refund_data = valid_full_refund_data.copy() + valid_full_refund_data['amount'] = 2000 + return valid_full_refund_data + + +@pytest.fixture +def provider(valid_capture_data, valid_full_refund_data): + _provider = RazorPayProvider(CLIENT_ID, SECRET) + mocked_payment = _provider.razorpay_client.payment = mock.MagicMock() + mocked_payment.capture.return_value = valid_capture_data + mocked_payment.refund.return_value = valid_full_refund_data + return _provider + + +@pytest.fixture +def valid_payment_form_data(): + return { + 'razorpay_payment_id': TRANSACTION_ID} diff --git a/tests/settings.py b/tests/settings.py new file mode 100644 index 0000000..961cd37 --- /dev/null +++ b/tests/settings.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +import os + +PROJECT_ROOT = os.path.normpath( + os.path.join(os.path.dirname(__file__), '..', 'django_payments_razorpay')) + +SECRET_KEY = 'secret' +PAYMENT_HOST = 'example.com' + +INSTALLED_APPS = ['payments', 'django.contrib.sites'] + +PAYMENT_VARIANTS = { + 'razorpay': ('django_payments_razorpay.RazorPayProvider', { + 'public_key': 'RAZORPAY_PUBLIC_KEY', + 'secret_key': 'RAZORPAY_SECRET_KEY'})} diff --git a/tests/test_forms.py b/tests/test_forms.py new file mode 100644 index 0000000..054e385 --- /dev/null +++ b/tests/test_forms.py @@ -0,0 +1,36 @@ +import pytest +from payments import PaymentStatus + +from django_payments_razorpay import ModalPaymentForm +from tests.conftest import TRANSACTION_ID, TOTAL_INT, TOTAL + + +def test_modal_payment_form_valid_data( + provider, payment, valid_payment_form_data): + form = ModalPaymentForm( + provider=provider, payment=payment, data=valid_payment_form_data) + assert form.is_valid() + provider.razorpay_client.payment.capture.assert_called_once_with( + TRANSACTION_ID, TOTAL_INT) + assert payment.captured_amount == TOTAL + assert payment.transaction_id == TRANSACTION_ID + assert payment.status == PaymentStatus.CONFIRMED + + +def test_modal_payment_form_already_processed( + provider, payment, valid_payment_form_data): + payment.transaction_id = TRANSACTION_ID + form = ModalPaymentForm( + provider=provider, payment=payment, data=valid_payment_form_data) + assert not form.is_valid() + provider.razorpay_client.payment.capture.assert_not_called() + + +def test_modal_payment_form_invalid_data( + provider, payment, valid_payment_form_data): + form = ModalPaymentForm( + provider=provider, payment=payment, data={}) + + with pytest.raises(KeyError, message='razorpay_payment_id'): + form.is_valid() + provider.razorpay_client.payment.capture.assert_not_called() diff --git a/tests/test_provider.py b/tests/test_provider.py new file mode 100644 index 0000000..7e23550 --- /dev/null +++ b/tests/test_provider.py @@ -0,0 +1,91 @@ +from decimal import Decimal + +import mock +import pytest +import razorpay.errors +from payments import PaymentStatus, RedirectNeeded +from payments.core import provider_factory + +from django_payments_razorpay import RazorPayProvider +from tests.conftest import CLIENT_ID, SECRET, TOTAL, TRANSACTION_ID + + +def test_provider_factory(): + assert isinstance(provider_factory('razorpay'), RazorPayProvider) + + +def test_authentication(provider): + assert provider.razorpay_client.auth == (CLIENT_ID, SECRET) + + +@mock.patch( + 'django_payments_razorpay.forms.RazorPayCheckoutWidget', create=True) +def test_get_form(mocked_razor_checkout, provider, payment): + form = provider.get_form(payment) + mocked_razor_checkout.assert_called_once_with( + provider=provider, payment=payment) + assert 'razorpay' in form.fields + assert form.fields['razorpay'].widget == mocked_razor_checkout.return_value + + +def test_get_form_invalid_data(provider, payment): + with pytest.raises(KeyError, message='razorpay_payment_id'): + provider.get_form(payment, data={}) + + assert payment.captured_amount == 0 + assert payment.transaction_id is None + + +def test_get_form_valid_data(valid_payment_form_data, provider, payment): + with pytest.raises(RedirectNeeded, message='https://success'): + provider.get_form(payment, data=valid_payment_form_data) + + assert payment.save.call_count != 0 + assert payment.status == PaymentStatus.CONFIRMED + assert payment.captured_amount == payment.total + assert payment.transaction_id == TRANSACTION_ID + + +@mock.patch('payments.models.provider_factory', create=True) +@pytest.mark.parametrize( + 'partial_refund,expected_status', ( + (False, PaymentStatus.REFUNDED), + (True, PaymentStatus.CONFIRMED) + )) +def test_refund( + mocked_provider_factory, + partial_refund, expected_status, + valid_partial_refund_data, provider, payment): + + mocked_provider_factory.return_value = provider + provider.refund = mock.MagicMock(wraps=provider.refund) + + if partial_refund: + refund_amount = Decimal(20) + expected_captured_amount = Decimal(200) + provider.razorpay_client.payment.refund.return_value = ( + valid_partial_refund_data) + else: + refund_amount = TOTAL + expected_captured_amount = 0 + + payment.captured_amount = payment.total + payment.status = PaymentStatus.CONFIRMED + payment.refund(amount=refund_amount) + + mocked_provider_factory.assert_called_once_with('razorpay') + provider.refund.assert_called_once_with(payment, refund_amount) + assert payment.captured_amount == expected_captured_amount + assert payment.status == expected_status + + +def test_refund_invalid_data(provider, payment): + def _raise_fake_error(*args, **kwargs): + raise razorpay.errors.BadRequestError('hello world') + payment.captured_amount = payment.total + provider.razorpay_client.payment.refund.side_effect = _raise_fake_error + + with pytest.raises(ValueError, message='hello world'): + provider.refund(payment, Decimal(2220)) + + assert payment.captured_amount == payment.total diff --git a/tests/test_widgets.py b/tests/test_widgets.py new file mode 100644 index 0000000..3cec29f --- /dev/null +++ b/tests/test_widgets.py @@ -0,0 +1,37 @@ +from django_payments_razorpay.widgets import RazorPayCheckoutWidget + + +def test_checkout_widget_attrs_overriding(provider, payment): + base_attrs = RazorPayCheckoutWidget(provider, payment).attrs + overridden_attrs = RazorPayCheckoutWidget( + provider, payment, attrs={'data-currency': 'INR'}).attrs + + base_attrs['data-currency'] = 'INR' + assert base_attrs == overridden_attrs + + +def test_checkout_widget_render_without_prefill(provider, payment): + widget = RazorPayCheckoutWidget(provider, payment) + assert widget.render() == ( + '') + + +def test_checkout_widget_render_with_prefill(provider, payment): + provider.prefill = True + widget = RazorPayCheckoutWidget(provider, payment) + assert widget.render() == ( + '') diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..89002b0 --- /dev/null +++ b/tox.ini @@ -0,0 +1,29 @@ +[tox] +envlist = py27-django111, py{34,35,36,37}-django{111,20,_master} + +[testenv] +usedevelop=True +deps= + coverage + django111: django>=1.11a1,<1.12 + django20: Django>=2.0a1,<2.1 + django_master: https://github.com/django/django/archive/master.tar.gz + mock + pytest + pytest-cov +commands=pytest --cov --cov-report= + +[travis] +python = + 2.7: py27 + 3.4: py34 + 3.5: py35 + 3.6: py36 + 3.7: py37 +unignore_outcomes = True + +[travis:env] +DJANGO = + 1.11: django111 + 2.0: django2.0 + master: django_master From a37582b3080e5fc5c48f9755d6c86a2f6b1750b3 Mon Sep 17 00:00:00 2001 From: NyanKiyoshi <6186720+NyanKiyoshi@users.noreply.github.com> Date: Fri, 10 Aug 2018 16:04:45 +0200 Subject: [PATCH 4/7] Exclude master of Python3.7, bump the version and add badges --- .travis.yml | 4 ++++ README.md | 5 +++++ setup.py | 5 +++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 610f2a7..bf26bcb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,6 +37,10 @@ matrix: env: DJANGO="2.0" - python: "3.6" env: DJANGO="master" + - python: "3.7" + env: DJANGO="master" + sudo: required + dist: xenial after_success: codecov install: pip install tox-travis codecov script: tox diff --git a/README.md b/README.md index 73eeae6..a627ff5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # Razorpay for django-payments +[![PyPi Release](https://img.shields.io/pypi/v/django-payments-razorpay.svg)](https://pypi.org/project/django-payments-razorpay/) +![python](https://img.shields.io/pypi/pyversions/django-payments-razorpay.svg) +[![Build Status](https://travis-ci.org/NyanKiyoshi/django-payment-razorpay.svg?branch=master)](https://travis-ci.org/NyanKiyoshi/django-payment-razorpay) +[![codecov](https://codecov.io/gh/NyanKiyoshi/django-payment-razorpay/branch/master/graph/badge.svg)](https://codecov.io/gh/NyanKiyoshi/django-payment-razorpay) + **WARNING:** only the paisa (INR) currency is supported by Razorpay as of now. ## Installation diff --git a/setup.py b/setup.py index 74b7ece..f9182db 100755 --- a/setup.py +++ b/setup.py @@ -10,9 +10,9 @@ name='django-payments-razorpay', author='NyanKiyoshi', author_email='hello@vanille.bid', - url='https://github.com/NyanKiyoshi/django-payments-razorpay', + url='https://github.com/NyanKiyoshi/django-payments-razorpay/', description='Razorpay provider for django-payments.', - version='0.0.0', + version='0.1.0', packages=['django_payments_razorpay'], include_package_data=True, classifiers=[ @@ -25,6 +25,7 @@ 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Framework :: Django', 'Topic :: Software Development :: Libraries :: Application Frameworks', 'Topic :: Software Development :: Libraries :: Python Modules'], From cb9fca1d083e9ab7e93bd4c9acd058ef6834b90f Mon Sep 17 00:00:00 2001 From: NyanKiyoshi <6186720+NyanKiyoshi@users.noreply.github.com> Date: Fri, 10 Aug 2018 16:07:38 +0200 Subject: [PATCH 5/7] Remove unneeded try import --- django_payments_razorpay/widgets.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/django_payments_razorpay/widgets.py b/django_payments_razorpay/widgets.py index d4bb950..7292873 100644 --- a/django_payments_razorpay/widgets.py +++ b/django_payments_razorpay/widgets.py @@ -1,12 +1,8 @@ +from django.forms.utils import flatatt from django.forms.widgets import HiddenInput from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ -try: - from django.forms.utils import flatatt -except ImportError: - from django.forms.util import flatatt - CHECKOUT_SCRIPT_URL = 'https://checkout.razorpay.com/v1/checkout.js' From e7040dbecd86e0e32e9e826dd4b8f14a1c40fad5 Mon Sep 17 00:00:00 2001 From: NyanKiyoshi <6186720+NyanKiyoshi@users.noreply.github.com> Date: Fri, 10 Aug 2018 16:39:57 +0200 Subject: [PATCH 6/7] isort imports, include django21 and 20 --- .travis.yml | 18 ++++++++++++------ setup.py | 12 +++++++++++- tests/conftest.py | 4 ++-- tests/settings.py | 1 + tests/test_forms.py | 5 ++--- tests/test_provider.py | 3 +-- tox.ini | 4 +++- 7 files changed, 32 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index bf26bcb..f7255b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ python: env: - DJANGO="1.11" - DJANGO="2.0" + - DJANGO="2.1" - DJANGO="master" matrix: include: @@ -25,22 +26,27 @@ matrix: dist: xenial env: DJANGO="master" allow_failures: - - python: "3.4" - env: DJANGO="2.0" - - python: "3.4" - env: DJANGO="master" - python: "3.5" - env: DJANGO="2.0" + env: DJANGO="2.1" - python: "3.5" env: DJANGO="master" - python: "3.6" - env: DJANGO="2.0" + env: DJANGO="2.1" - python: "3.6" env: DJANGO="master" + - python: "3.7" + sudo: required + dist: xenial + env: DJANGO="2.1" - python: "3.7" env: DJANGO="master" sudo: required dist: xenial + exclude: + - python: "3.4" + env: DJANGO="2.1" + - python: "3.4" + env: DJANGO="master" after_success: codecov install: pip install tox-travis codecov script: tox diff --git a/setup.py b/setup.py index f9182db..222243c 100755 --- a/setup.py +++ b/setup.py @@ -1,18 +1,28 @@ #!/usr/bin/env python +from os.path import isfile from setuptools import setup REQUIREMENTS = ['django-payments>=0.12.3', 'razorpay>=1.1.1'] TEST_REQUIREMENTS = ['pytest', 'mock'] +if isfile('README.md'): + with open('README.md') as fp: + long_description = fp.read() +else: + long_description = '' + + setup( name='django-payments-razorpay', author='NyanKiyoshi', author_email='hello@vanille.bid', url='https://github.com/NyanKiyoshi/django-payments-razorpay/', description='Razorpay provider for django-payments.', - version='0.1.0', + long_description=long_description, + long_description_content_type='text/markdown', + version='0.1.1', packages=['django_payments_razorpay'], include_package_data=True, classifiers=[ diff --git a/tests/conftest.py b/tests/conftest.py index 40fb747..af8fed8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,10 @@ from decimal import Decimal -import pytest import mock +import pytest +from django_payments_razorpay import RazorPayProvider from payments import PaymentStatus from payments.models import BasePayment -from django_payments_razorpay import RazorPayProvider TRANSACTION_ID = 'pay_7IZD7aJ2kkmOjk' diff --git a/tests/settings.py b/tests/settings.py index 961cd37..74870a1 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals + import os PROJECT_ROOT = os.path.normpath( diff --git a/tests/test_forms.py b/tests/test_forms.py index 054e385..0fac558 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -1,8 +1,7 @@ import pytest -from payments import PaymentStatus - from django_payments_razorpay import ModalPaymentForm -from tests.conftest import TRANSACTION_ID, TOTAL_INT, TOTAL +from payments import PaymentStatus +from tests.conftest import TOTAL, TOTAL_INT, TRANSACTION_ID def test_modal_payment_form_valid_data( diff --git a/tests/test_provider.py b/tests/test_provider.py index 7e23550..a1a46de 100644 --- a/tests/test_provider.py +++ b/tests/test_provider.py @@ -3,10 +3,9 @@ import mock import pytest import razorpay.errors +from django_payments_razorpay import RazorPayProvider from payments import PaymentStatus, RedirectNeeded from payments.core import provider_factory - -from django_payments_razorpay import RazorPayProvider from tests.conftest import CLIENT_ID, SECRET, TOTAL, TRANSACTION_ID diff --git a/tox.ini b/tox.ini index 89002b0..638b9e5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27-django111, py{34,35,36,37}-django{111,20,_master} +envlist = py27-django111, py{34,35,36,37}-django{111,20,21,_master} [testenv] usedevelop=True @@ -7,6 +7,7 @@ deps= coverage django111: django>=1.11a1,<1.12 django20: Django>=2.0a1,<2.1 + django21: Django>=2.1,<2.2 django_master: https://github.com/django/django/archive/master.tar.gz mock pytest @@ -26,4 +27,5 @@ unignore_outcomes = True DJANGO = 1.11: django111 2.0: django2.0 + 2.1: django2.1 master: django_master From 54832c47bee70c0b7881273dfdeeae252964b68e Mon Sep 17 00:00:00 2001 From: NyanKiyoshi <6186720+NyanKiyoshi@users.noreply.github.com> Date: Fri, 10 Aug 2018 16:47:15 +0200 Subject: [PATCH 7/7] Fix typo in readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a627ff5..72abce2 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![PyPi Release](https://img.shields.io/pypi/v/django-payments-razorpay.svg)](https://pypi.org/project/django-payments-razorpay/) ![python](https://img.shields.io/pypi/pyversions/django-payments-razorpay.svg) -[![Build Status](https://travis-ci.org/NyanKiyoshi/django-payment-razorpay.svg?branch=master)](https://travis-ci.org/NyanKiyoshi/django-payment-razorpay) -[![codecov](https://codecov.io/gh/NyanKiyoshi/django-payment-razorpay/branch/master/graph/badge.svg)](https://codecov.io/gh/NyanKiyoshi/django-payment-razorpay) +[![Build Status](https://travis-ci.org/NyanKiyoshi/django-payment-razorpay.svg?branch=master)](https://travis-ci.org/NyanKiyoshi/django-payments-razorpay) +[![codecov](https://codecov.io/gh/NyanKiyoshi/django-payment-razorpay/branch/master/graph/badge.svg)](https://codecov.io/gh/NyanKiyoshi/django-payments-razorpay) **WARNING:** only the paisa (INR) currency is supported by Razorpay as of now.