├── robokassa
├── __init__.py
├── migrations
│ ├── __init__.py
│ └── 0001_initial.py
├── signals.py
├── urls.py
├── models.py
├── conf.py
├── tests.py
├── views.py
└── forms.py
├── example_project
├── app
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ └── 0001_initial.py
│ ├── tests.py
│ ├── admin.py
│ ├── apps.py
│ ├── templates
│ │ ├── robokassa
│ │ │ ├── fail.html
│ │ │ ├── success.html
│ │ │ └── error.html
│ │ └── app
│ │ │ ├── base.html
│ │ │ └── pay_with_robokassa.html
│ ├── views.py
│ └── models.py
├── example_project
│ ├── __init__.py
│ ├── wsgi.py
│ ├── urls.py
│ └── settings.py
└── manage.py
├── MANIFEST.in
├── .gitignore
├── .hgtags
├── tox.ini
├── .hgignore
├── runtests.py
├── LICENSE.txt
├── setup.py
├── CHANGES.rst
└── README.rst
/robokassa/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example_project/app/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/robokassa/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example_project/app/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example_project/example_project/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.txt
2 | include *.rst
3 | recursive-include docs *.txt
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .tox
2 | *.pyc
3 | __pycache__/
4 | MANIFEST
5 | build/
6 | dist/
7 | .idea/
8 | *.sqlite3
9 | *.bak
10 | *.egg-info/
11 |
--------------------------------------------------------------------------------
/example_project/app/tests.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.test import TestCase
5 |
6 | # Create your tests here.
7 |
--------------------------------------------------------------------------------
/example_project/app/admin.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.contrib import admin
5 |
6 | # Register your models here.
7 |
--------------------------------------------------------------------------------
/example_project/app/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.apps import AppConfig
5 |
6 |
7 | class AppConfig(AppConfig):
8 | name = 'app'
9 |
--------------------------------------------------------------------------------
/example_project/app/templates/robokassa/fail.html:
--------------------------------------------------------------------------------
1 | {% extends 'app/base.html' %}
2 |
3 | {% block title %}
4 | Неудача
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
Неудача
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/example_project/app/templates/robokassa/success.html:
--------------------------------------------------------------------------------
1 | {% extends 'app/base.html' %}
2 |
3 | {% block title %}
4 | Успех
5 | {% endblock %}
6 |
7 | {% block content %}
8 | Успех
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/example_project/app/templates/robokassa/error.html:
--------------------------------------------------------------------------------
1 | {% extends 'app/base.html' %}
2 |
3 | {% block title %}
4 | Ошибка на стороне сервера
5 | {% endblock %}
6 |
7 | {% block content %}
8 | Ошибка на стороне сервера
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/example_project/app/templates/app/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% block title %}{% endblock %}
6 |
7 |
8 | {% block content %}{% endblock %}
9 |
10 |
--------------------------------------------------------------------------------
/example_project/app/templates/app/pay_with_robokassa.html:
--------------------------------------------------------------------------------
1 | {% extends 'app/base.html' %}
2 |
3 | {% block content %}
4 |
8 | {% endblock %}
--------------------------------------------------------------------------------
/robokassa/signals.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | from __future__ import unicode_literals
3 |
4 | from django.dispatch import Signal
5 |
6 |
7 | result_received = Signal(providing_args=["InvId", "OutSum"])
8 | success_page_visited = Signal(providing_args=["InvId", "OutSum"])
9 | fail_page_visited = Signal(providing_args=["InvId", "OutSum"])
10 |
--------------------------------------------------------------------------------
/.hgtags:
--------------------------------------------------------------------------------
1 | d76fe387d6227fa7e5409f19d0c1818a8cfe579c 0.9.0
2 | 9f32acddc224e6e94c472805d9fee248c31c42d5 0.9.1
3 | 064e8588576a229fcbe22a8c507abfd2ee3850b9 0.9.2
4 | 0e224e5f37776c80d2fa7fda2c25fed303246bb8 0.9.3
5 | db88f5aeabab6de837326a6712b37e6a8986afc8 1.0
6 | af1b392e0048d7353f083ba73ef2d4c329fbf5f5 1.1
7 | 951f830faab4586eb83775ddceca86cef203943f 1.2
8 |
--------------------------------------------------------------------------------
/robokassa/urls.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | from __future__ import unicode_literals
3 |
4 | from django.conf.urls import url
5 |
6 | from . import views
7 |
8 | app_name = 'robokassa'
9 |
10 | urlpatterns = [
11 | url(r'^result/$', views.receive_result, name='result'),
12 | url(r'^success/$', views.success, name='success'),
13 | url(r'^fail/$', views.fail, name='fail'),
14 | ]
15 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py26,py27,pypy,dj13,dj15,dj16
3 |
4 | [testenv]
5 | deps=
6 | django >= 1.4, < 1.5
7 | south
8 |
9 | commands=
10 | python runtests.py
11 |
12 | [testenv:dj13]
13 | deps =
14 | django >= 1.3, < 1.4
15 | south
16 |
17 | [testenv:dj15]
18 | deps =
19 | django >= 1.5, < 1.6
20 | south
21 |
22 | [testenv:dj16]
23 | deps =
24 | django >= 1.6, < 1.7
25 | south
26 |
--------------------------------------------------------------------------------
/.hgignore:
--------------------------------------------------------------------------------
1 | syntax: glob
2 |
3 | #IDE files
4 | .settings/*
5 | .project
6 | .pydevproject
7 | .cache/*
8 | nbproject/*
9 | .buildpath
10 | build.properties
11 | .idea/*
12 |
13 | #temp files
14 | *.pyc
15 | *.pyo
16 | *.orig
17 | *~
18 |
19 | #misc files
20 | pip-log.txt
21 |
22 | #os files
23 | .DS_Store
24 | Thumbs.db
25 |
26 | #setup files
27 | build/
28 | dist/
29 | .build/
30 | MANIFEST
31 | django_robokassa.egg-info
32 | .tox
33 |
--------------------------------------------------------------------------------
/example_project/example_project/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for example_project project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/runtests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 | from __future__ import unicode_literals
4 |
5 | import django
6 | from django.conf import settings
7 | from django.core.management import call_command
8 |
9 | settings.configure(
10 | INSTALLED_APPS=('robokassa',),
11 | DATABASE_ENGINE='sqlite3',
12 | DATABASES={
13 | 'default': {
14 | 'ENGINE': 'django.db.backends.sqlite3',
15 | },
16 | },
17 |
18 | ROBOKASSA_LOGIN='test_login',
19 | ROBOKASSA_PASSWORD1='test_password',
20 | ROBOKASSA_PASSWORD2='test_password2',
21 | ROBOKASSA_EXTRA_PARAMS=['param1', 'param2'],
22 | )
23 |
24 | django.setup()
25 |
26 | if __name__ == "__main__":
27 | call_command('test', 'robokassa')
28 |
--------------------------------------------------------------------------------
/example_project/app/views.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | from __future__ import unicode_literals
3 |
4 | from django.shortcuts import get_object_or_404, render
5 | from django.contrib.auth.decorators import login_required
6 | from robokassa.forms import RobokassaForm
7 |
8 | from .models import Order
9 |
10 |
11 | @login_required
12 | def pay_with_robokassa(request, order_id):
13 | order = get_object_or_404(Order, pk=order_id)
14 |
15 | form = RobokassaForm(initial={
16 | 'OutSum': order.total,
17 | 'InvId': order.id,
18 | 'Desc': order.name,
19 | 'Email': request.user.email,
20 | # 'IncCurrLabel': '',
21 | # 'Culture': 'ru'
22 | })
23 |
24 | return render(request, 'app/pay_with_robokassa.html', {'form': form})
25 |
--------------------------------------------------------------------------------
/robokassa/models.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models
5 | from django.utils.six import python_2_unicode_compatible
6 |
7 |
8 | @python_2_unicode_compatible
9 | class SuccessNotification(models.Model):
10 | InvId = models.IntegerField('Номер заказа', db_index=True)
11 | OutSum = models.CharField('Сумма', max_length=15)
12 |
13 | created_at = models.DateTimeField('Дата и время получения уведомления', auto_now_add=True)
14 |
15 | class Meta:
16 | verbose_name = 'Уведомление об успешном платеже'
17 | verbose_name_plural = 'Уведомления об успешных платежах (ROBOKASSA)'
18 |
19 | def __str__(self):
20 | return '#{}: {} ({})'.format(self.InvId, self.OutSum, self.created_at)
21 |
--------------------------------------------------------------------------------
/example_project/app/models.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models
5 | from django.dispatch import receiver
6 | from robokassa.signals import result_received
7 |
8 |
9 | class Order(models.Model):
10 | name = models.CharField(max_length=255)
11 | total = models.DecimalField(max_digits=15, decimal_places=2)
12 | status = models.CharField(max_length=255, blank=True, null=True)
13 | paid_sum = models.DecimalField(max_digits=15, decimal_places=2, blank=True, null=True)
14 | extra_param = models.CharField(max_length=255, blank=True, null=True)
15 |
16 |
17 | @receiver(result_received)
18 | def payment_received(sender, **kwargs):
19 | order = Order.objects.get(pk=kwargs['InvId'])
20 | order.status = 'paid'
21 | order.paid_sum = kwargs['OutSum']
22 | order.extra_param = kwargs['extra']['my_param']
23 | order.save()
24 |
--------------------------------------------------------------------------------
/example_project/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | # Add our app to PYTHONPATH
6 | sys.path.append(os.path.join(os.path.dirname(__file__), '../'))
7 |
8 | if __name__ == "__main__":
9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError:
13 | # The above import may fail for some other reason. Ensure that the
14 | # issue is really that Django is missing to avoid masking other
15 | # exceptions on Python 2.
16 | try:
17 | import django
18 | except ImportError:
19 | raise ImportError(
20 | "Couldn't import Django. Are you sure it's installed and "
21 | "available on your PYTHONPATH environment variable? Did you "
22 | "forget to activate a virtual environment?"
23 | )
24 | raise
25 | execute_from_command_line(sys.argv)
26 |
--------------------------------------------------------------------------------
/example_project/example_project/urls.py:
--------------------------------------------------------------------------------
1 | """example_project URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.conf.urls import url, include
14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
15 | """
16 | from django.conf.urls import url, include
17 | from django.contrib import admin
18 |
19 | from app import views
20 |
21 | urlpatterns = [
22 | url(r'^admin/', admin.site.urls),
23 | url(r'^robokassa/', include('robokassa.urls')),
24 | url(r'^pay/(?P\d+)/$', views.pay_with_robokassa),
25 | ]
26 |
--------------------------------------------------------------------------------
/example_project/app/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11 on 2018-04-26 13:22
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='Order',
18 | fields=[
19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20 | ('name', models.CharField(max_length=255)),
21 | ('total', models.DecimalField(decimal_places=2, max_digits=15)),
22 | ('status', models.CharField(blank=True, max_length=255, null=True)),
23 | ('paid_sum', models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True)),
24 | ('extra_param', models.CharField(blank=True, max_length=255, null=True)),
25 | ],
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010 Mikhail Korobov
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
--------------------------------------------------------------------------------
/robokassa/conf.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | from __future__ import unicode_literals
3 |
4 | from django.conf import settings
5 |
6 | # обязательные параметры - реквизиты магазина
7 | LOGIN = settings.ROBOKASSA_LOGIN
8 | PASSWORD1 = settings.ROBOKASSA_PASSWORD1
9 | PASSWORD2 = getattr(settings, 'ROBOKASSA_PASSWORD2', None)
10 |
11 | # использовать ли метод POST при приеме результатов
12 | USE_POST = getattr(settings, 'ROBOKASSA_USE_POST', True)
13 |
14 | # требовать предварительного уведомления на ResultURL
15 | STRICT_CHECK = getattr(settings, 'ROBOKASSA_STRICT_CHECK', True)
16 |
17 | # тестовый режим
18 | TEST_MODE = getattr(settings, 'ROBOKASSA_TEST_MODE', False)
19 |
20 | # url, по которому будет идти отправка форм
21 | FORM_TARGET = 'https://merchant.roboxchange.com/Index.aspx'
22 |
23 | if TEST_MODE:
24 | FORM_TARGET = getattr(
25 | settings,
26 | 'ROBOKASSA_TEST_FORM_TARGET',
27 | 'https://auth.robokassa.ru/Merchant/Index.aspx'
28 | )
29 |
30 | # список пользовательских параметров ("shp" к ним приписывать не нужно)
31 | EXTRA_PARAMS = sorted(getattr(settings, 'ROBOKASSA_EXTRA_PARAMS', []))
32 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 | from __future__ import unicode_literals
4 |
5 | from setuptools import setup
6 |
7 |
8 | setup(
9 | name='django-robokassa3',
10 | version='1.4',
11 | author='Mikhail Pyrev',
12 | author_email='mikhail.pyrev@gmail.com',
13 |
14 | packages=['robokassa', 'robokassa.migrations'],
15 |
16 | url='https://github.com/mpyrev/django-robokassa',
17 | license='MIT License',
18 | description='Приложение для интеграции платежной системы ROBOKASSA в проекты на Django.',
19 | long_description=open('README.rst').read() + "\n\n" + open('CHANGES.rst').read(),
20 |
21 | install_requires=[
22 | 'Django>=1.11',
23 | 'six',
24 | ],
25 |
26 | classifiers=(
27 | 'Environment :: Web Environment',
28 | 'Framework :: Django',
29 | 'Framework :: Django :: 1.11',
30 | 'Framework :: Django :: 2.0',
31 | 'Intended Audience :: Developers',
32 | 'License :: OSI Approved :: MIT License',
33 | 'Programming Language :: Python',
34 | 'Programming Language :: Python :: 2',
35 | 'Programming Language :: Python :: 2.7',
36 | 'Programming Language :: Python :: 3',
37 | 'Programming Language :: Python :: 3.4',
38 | 'Programming Language :: Python :: 3.5',
39 | 'Programming Language :: Python :: 3.6',
40 | 'Programming Language :: Python :: 3.7',
41 | 'Topic :: Software Development :: Libraries :: Python Modules',
42 | 'Natural Language :: Russian',
43 | ),
44 | )
45 |
--------------------------------------------------------------------------------
/robokassa/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11 on 2018-04-26 12:33
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='SuccessNotification',
18 | fields=[
19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20 | ('InvId', models.IntegerField(db_index=True, verbose_name='\u041d\u043e\u043c\u0435\u0440 \u0437\u0430\u043a\u0430\u0437\u0430')),
21 | ('OutSum', models.CharField(max_length=15, verbose_name='\u0421\u0443\u043c\u043c\u0430')),
22 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='\u0414\u0430\u0442\u0430 \u0438 \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f')),
23 | ],
24 | options={
25 | 'verbose_name': '\u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u043e\u0431 \u0443\u0441\u043f\u0435\u0448\u043d\u043e\u043c \u043f\u043b\u0430\u0442\u0435\u0436\u0435',
26 | 'verbose_name_plural': '\u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f \u043e\u0431 \u0443\u0441\u043f\u0435\u0448\u043d\u044b\u0445 \u043f\u043b\u0430\u0442\u0435\u0436\u0430\u0445 (ROBOKASSA)',
27 | },
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/CHANGES.rst:
--------------------------------------------------------------------------------
1 |
2 | История изменений
3 | =================
4 |
5 | 1.4 (2018-04-26)
6 | ----------------
7 |
8 | * Добавлена поддержка Python 3 (3.4, 3.5, 3.6, 3.7)
9 | * Добавлена поддержка последних версий Django (1.11, 2.0)
10 | * Добавлен тестовый проект example_project
11 | * Миграции заменены на стандартные (вместо South)
12 | * Код подчищен для большего соответствия PEP-8
13 |
14 | 1.3 (2016-03-07)
15 | ----------------
16 |
17 | * исправлена работа в тестовом режиме (спасибо Шумихину Ивану);
18 | * настройка ROBOKASSA_TEST_FORM_TARGET позволяет указать свой тестовый сервер
19 | (спасибо https://github.com/superqwer).
20 | * репозиторий на bitbucket больше не поддерживается;
21 | * setup.py теперь использует setuptools.
22 |
23 | 1.2 (2013-12-24)
24 | ----------------
25 |
26 | * Добавлена поддержка django 1.6 - спасибо Александру Симкину;
27 | * Python 2.5 больше не поддерживается.
28 |
29 | 1.1 (2013-04-12)
30 | ----------------
31 |
32 | * На странице FailURL больше не проверяется подпись (т.к. Робокасса ее
33 | больше не передает) - спасибо @amureki;
34 | * улучшена справка - спасибо @bo858585.
35 |
36 | 1.0 (2012-03-24)
37 | ----------------
38 | * Для работы теперь требуется django >= 1.3;
39 | * добавлена поддержка django 1.4;
40 | * все вьюхи возвращают теперь TemplateResponse;
41 | * миграции переведены на современную версию south;
42 | * запуск тестов через tox;
43 | * небольшие улучшения в README.
44 |
45 | 0.9.3 (2010-08-05)
46 | ------------------
47 | Сообщения с ошибочной подписью не вызывают исключения.
48 |
49 | 0.9.2 (2010-06-23)
50 | ------------------
51 | Добавлена поддержка django 1.2.
52 |
53 | 0.9.1 (2010-05-10)
54 | ------------------
55 | Исправлена работа с дополнительными (пользовательскими) параметрами.
56 |
57 | 0.9.0 (2010-04-15)
58 | ------------------
59 | Первая версия
60 |
61 |
--------------------------------------------------------------------------------
/robokassa/tests.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | from __future__ import unicode_literals
3 |
4 | from unittest import TestCase
5 | from django.test import TestCase as DjangoTestCase
6 | from robokassa.forms import RobokassaForm, ResultURLForm
7 | from robokassa.conf import LOGIN, PASSWORD1, PASSWORD2
8 |
9 |
10 | class RobokassaFormTest(TestCase):
11 |
12 | def setUp(self):
13 | self.form = RobokassaForm(initial={
14 | 'OutSum': 100.00,
15 | 'InvId': 58,
16 | 'Desc' : 'Холодильник "Бирюса"',
17 | 'Email' : 'vasia@example.com'
18 | })
19 |
20 | def testSignature(self):
21 | self.assertEqual(self.form._get_signature_string(),
22 | '%s:100.0:58:%s:shpparam1=None:shpparam2=None' % (LOGIN, PASSWORD1))
23 | self.assertEqual(len(self.form.fields['SignatureValue'].initial), 32)
24 |
25 | def testSignatureMissingParams(self):
26 | form = RobokassaForm(initial = {'InvId': 5})
27 | self.assertEqual(form._get_signature_string(),
28 | '%s::5:%s:shpparam1=None:shpparam2=None' % (LOGIN, PASSWORD1))
29 |
30 | def testRedirectUrl(self):
31 | url = "https://merchant.roboxchange.com/Index.aspx?MrchLogin=test_login&OutSum=100.0&InvId=58&Desc=%D5%EE%EB%EE%E4%E8%EB%FC%ED%E8%EA+%22%C1%E8%F0%FE%F1%E0%22&SignatureValue=0EC23BE40003640B35EC07F6615FFB57&Email=vasia%40example.com&shpparam1=None&shpparam2=None"
32 | self.assertEqual(self.form.get_redirect_url(), url)
33 |
34 |
35 | class RobokassaFormExtraTest(TestCase):
36 | def testExtra(self):
37 | form = RobokassaForm(initial={
38 | 'InvId': 58,
39 | 'OutSum': 100,
40 | 'param1': 'value1',
41 | 'param2': 'value2'
42 | })
43 | self.assertEqual(form._get_signature_string(),
44 | '%s:100:58:%s:shpparam1=value1:shpparam2=value2' % (LOGIN, PASSWORD1))
45 |
46 |
47 | class ResultURLTest(DjangoTestCase):
48 |
49 | def testFormExtra(self):
50 | form = ResultURLForm({
51 | 'OutSum': '100',
52 | 'InvId': '58',
53 | 'SignatureValue': 'B2111A06F6B7A1E090D38367BF7032D9',
54 | 'shpparam1': 'Vasia',
55 | 'shpparam2': 'None',
56 | })
57 | self.assertTrue(form.is_valid())
58 | self.assertEqual(form._get_signature_string(),
59 | '100:58:%s:shpparam1=Vasia:shpparam2=None' % (PASSWORD2))
60 | self.assertEqual(form.extra_params(), {'param1': 'Vasia', 'param2': 'None'})
61 |
62 |
63 | def testFormValid(self):
64 |
65 | self.assertTrue(ResultURLForm({
66 | 'OutSum': '100',
67 | 'InvId': '58',
68 | 'SignatureValue': '877D3BAF8381F70E56638C3BC82580C5',
69 | 'shpparam1': 'None',
70 | 'shpparam2': 'None',
71 | }).is_valid())
72 |
73 | self.assertFalse(ResultURLForm({
74 | 'OutSum': '101',
75 | 'InvId': '58',
76 | 'SignatureValue': '877D3BAF8381F70E56638C3BC82580C5',
77 | 'shpparam1': 'None',
78 | 'shpparam2': 'None',
79 | }).is_valid())
80 |
81 | self.assertFalse(ResultURLForm({
82 | 'OutSum': '100',
83 | 'InvId': '58',
84 | 'SignatureValue': '877D3BAF8381F70E56638C3BC82580C5',
85 | 'shpparam1': 'Vasia',
86 | 'shpparam2': 'None',
87 | }).is_valid())
88 |
89 | def testEmptyFormValid(self):
90 | self.assertFalse(ResultURLForm().is_valid())
91 |
--------------------------------------------------------------------------------
/robokassa/views.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | from __future__ import unicode_literals
3 |
4 | from django.http import HttpResponse
5 | from django.template.response import TemplateResponse
6 | from django.views.decorators.csrf import csrf_exempt
7 |
8 | from robokassa.conf import USE_POST
9 | from robokassa.forms import ResultURLForm, SuccessRedirectForm, FailRedirectForm
10 | from robokassa.models import SuccessNotification
11 | from robokassa.signals import result_received, success_page_visited, fail_page_visited
12 |
13 |
14 | @csrf_exempt
15 | def receive_result(request):
16 | """Обработчик для ResultURL."""
17 | data = request.POST if USE_POST else request.GET
18 | form = ResultURLForm(data)
19 | if form.is_valid():
20 | inv_id, out_sum = form.cleaned_data['InvId'], form.cleaned_data['OutSum']
21 |
22 | # сохраняем данные об успешном уведомлении в базе, чтобы
23 | # можно было выполнить дополнительную проверку на странице успешного
24 | # заказа
25 | notification = SuccessNotification.objects.create(InvId=inv_id, OutSum=out_sum)
26 |
27 | # дополнительные действия с заказом (например, смену его статуса) можно
28 | # осуществить в обработчике сигнала robokassa.signals.result_received
29 | result_received.send(sender=notification, InvId=inv_id, OutSum=out_sum,
30 | extra=form.extra_params())
31 |
32 | return HttpResponse('OK%s' % inv_id)
33 | return HttpResponse('error: bad signature')
34 |
35 |
36 | @csrf_exempt
37 | def success(request, template_name='robokassa/success.html', extra_context=None,
38 | error_template_name='robokassa/error.html'):
39 | """Обработчик для SuccessURL"""
40 |
41 | data = request.POST if USE_POST else request.GET
42 | form = SuccessRedirectForm(data)
43 | if form.is_valid():
44 | inv_id, out_sum = form.cleaned_data['InvId'], form.cleaned_data['OutSum']
45 |
46 | # в случае, когда не используется строгая проверка, действия с заказом
47 | # можно осуществлять в обработчике сигнала robokassa.signals.success_page_visited
48 | success_page_visited.send(sender=form, InvId=inv_id, OutSum=out_sum,
49 | extra=form.extra_params())
50 |
51 | context = {'InvId': inv_id, 'OutSum': out_sum, 'form': form}
52 | context.update(form.extra_params())
53 | context.update(extra_context or {})
54 | return TemplateResponse(request, template_name, context)
55 |
56 | return TemplateResponse(request, error_template_name, {'form': form})
57 |
58 |
59 | @csrf_exempt
60 | def fail(request, template_name='robokassa/fail.html', extra_context=None,
61 | error_template_name='robokassa/error.html'):
62 | """Обработчик для FailURL"""
63 |
64 | data = request.POST if USE_POST else request.GET
65 | form = FailRedirectForm(data)
66 | if form.is_valid():
67 | inv_id, out_sum = form.cleaned_data['InvId'], form.cleaned_data['OutSum']
68 |
69 | # дополнительные действия с заказом (например, смену его статуса для
70 | # разблокировки товара на складе) можно осуществить в обработчике
71 | # сигнала robokassa.signals.fail_page_visited
72 | fail_page_visited.send(sender=form, InvId=inv_id, OutSum=out_sum,
73 | extra=form.extra_params())
74 |
75 | context = {'InvId': inv_id, 'OutSum': out_sum, 'form': form}
76 | context.update(form.extra_params())
77 | context.update(extra_context or {})
78 | return TemplateResponse(request, template_name, context)
79 |
80 | return TemplateResponse(request, error_template_name, {'form': form})
81 |
82 |
--------------------------------------------------------------------------------
/example_project/example_project/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for example_project project.
3 |
4 | Generated by 'django-admin startproject' using Django 1.11.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.11/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/1.11/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17 |
18 |
19 | # Quick-start development settings - unsuitable for production
20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
21 |
22 | # SECURITY WARNING: keep the secret key used in production secret!
23 | SECRET_KEY = 'hl@couow8+w8sbk@)+#i!@*h^073p7z=@k7gjipu6v7h17y8az'
24 |
25 | # SECURITY WARNING: don't run with debug turned on in production!
26 | DEBUG = True
27 |
28 | ALLOWED_HOSTS = []
29 |
30 |
31 | # Application definition
32 |
33 | INSTALLED_APPS = [
34 | 'django.contrib.admin',
35 | 'django.contrib.auth',
36 | 'django.contrib.contenttypes',
37 | 'django.contrib.sessions',
38 | 'django.contrib.messages',
39 | 'django.contrib.staticfiles',
40 |
41 | 'robokassa',
42 | 'app',
43 | ]
44 |
45 | MIDDLEWARE = [
46 | 'django.middleware.security.SecurityMiddleware',
47 | 'django.contrib.sessions.middleware.SessionMiddleware',
48 | 'django.middleware.common.CommonMiddleware',
49 | 'django.middleware.csrf.CsrfViewMiddleware',
50 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
51 | 'django.contrib.messages.middleware.MessageMiddleware',
52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
53 | ]
54 |
55 | ROOT_URLCONF = 'example_project.urls'
56 |
57 | TEMPLATES = [
58 | {
59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
60 | 'DIRS': [],
61 | 'APP_DIRS': True,
62 | 'OPTIONS': {
63 | 'context_processors': [
64 | 'django.template.context_processors.debug',
65 | 'django.template.context_processors.request',
66 | 'django.contrib.auth.context_processors.auth',
67 | 'django.contrib.messages.context_processors.messages',
68 | ],
69 | },
70 | },
71 | ]
72 |
73 | WSGI_APPLICATION = 'example_project.wsgi.application'
74 |
75 |
76 | # Database
77 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases
78 |
79 | DATABASES = {
80 | 'default': {
81 | 'ENGINE': 'django.db.backends.sqlite3',
82 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
83 | }
84 | }
85 |
86 |
87 | # Password validation
88 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
89 |
90 | AUTH_PASSWORD_VALIDATORS = [
91 | {
92 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
93 | },
94 | {
95 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
96 | },
97 | {
98 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
99 | },
100 | {
101 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
102 | },
103 | ]
104 |
105 |
106 | # Internationalization
107 | # https://docs.djangoproject.com/en/1.11/topics/i18n/
108 |
109 | LANGUAGE_CODE = 'en-us'
110 |
111 | TIME_ZONE = 'UTC'
112 |
113 | USE_I18N = True
114 |
115 | USE_L10N = True
116 |
117 | USE_TZ = True
118 |
119 |
120 | # Static files (CSS, JavaScript, Images)
121 | # https://docs.djangoproject.com/en/1.11/howto/static-files/
122 |
123 | STATIC_URL = '/static/'
124 |
125 | ROBOKASSA_LOGIN = 'test_login'
126 | ROBOKASSA_PASSWORD1 = 'test_password'
127 | ROBOKASSA_PASSWORD2 = 'test_password2'
128 | ROBOKASSA_EXTRA_PARAMS = ['param1', 'param2']
129 |
--------------------------------------------------------------------------------
/robokassa/forms.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | from __future__ import unicode_literals
3 |
4 | from hashlib import md5
5 | from six.moves.urllib.parse import urlencode
6 |
7 | import six
8 | from django import forms
9 |
10 | from robokassa.conf import LOGIN, PASSWORD1, PASSWORD2, TEST_MODE, STRICT_CHECK, FORM_TARGET, EXTRA_PARAMS
11 | from robokassa.models import SuccessNotification
12 |
13 |
14 | class BaseRobokassaForm(forms.Form):
15 | def __init__(self, *args, **kwargs):
16 | super(BaseRobokassaForm, self).__init__(*args, **kwargs)
17 | # создаем дополнительные поля
18 | for key in EXTRA_PARAMS:
19 | self.fields['shp'+key] = forms.CharField(required=False)
20 | if 'initial' in kwargs:
21 | self.fields['shp'+key].initial = kwargs['initial'].get(key, 'None')
22 |
23 | def _append_extra_part(self, standard_part, value_func):
24 | extra_part = ":".join(["%s=%s" % ('shp'+key, value_func('shp' + key)) for key in EXTRA_PARAMS])
25 | if extra_part:
26 | return ':'.join([standard_part, extra_part])
27 | return standard_part
28 |
29 | def extra_params(self):
30 | extra = {}
31 | for param in EXTRA_PARAMS:
32 | if ('shp'+param) in self.cleaned_data:
33 | extra[param] = self.cleaned_data['shp'+param]
34 | return extra
35 |
36 | def _get_signature(self):
37 | return md5(self._get_signature_string().encode('utf-8')).hexdigest().upper()
38 |
39 | def _get_signature_string(self):
40 | raise NotImplementedError
41 |
42 |
43 | class RobokassaForm(BaseRobokassaForm):
44 | # login магазина в обменном пункте
45 | MrchLogin = forms.CharField(max_length=20, initial=LOGIN)
46 |
47 | # требуемая к получению сумма
48 | OutSum = forms.DecimalField(min_value=0, max_digits=20, decimal_places=2, required=False)
49 |
50 | # номер счета в магазине (должен быть уникальным для магазина)
51 | InvId = forms.IntegerField(min_value=0, required=False)
52 |
53 | # описание покупки
54 | Desc = forms.CharField(max_length=100, required=False)
55 |
56 | # контрольная сумма MD5
57 | SignatureValue = forms.CharField(max_length=32)
58 |
59 | # предлагаемая валюта платежа
60 | IncCurrLabel = forms.CharField(max_length=10, required=False)
61 |
62 | # e-mail пользователя
63 | Email = forms.CharField(max_length=100, required=False)
64 |
65 | # язык общения с клиентом (en или ru)
66 | Culture = forms.CharField(max_length=10, required=False)
67 |
68 | # Параметр с URL'ом, на который форма должны быть отправлена.
69 | # Может пригодиться для использования в шаблоне.
70 | target = FORM_TARGET
71 |
72 | def __init__(self, *args, **kwargs):
73 |
74 | super(RobokassaForm, self).__init__(*args, **kwargs)
75 |
76 | if TEST_MODE is True:
77 | self.fields['isTest'] = forms.BooleanField(required=False)
78 | self.fields['isTest'].initial = 1
79 |
80 | # скрытый виджет по умолчанию
81 | for field in self.fields:
82 | self.fields[field].widget = forms.HiddenInput()
83 |
84 | self.fields['SignatureValue'].initial = self._get_signature()
85 |
86 | def get_redirect_url(self):
87 | """ Получить URL с GET-параметрами, соответствующими значениям полей в
88 | форме. Редирект на адрес, возвращаемый этим методом, эквивалентен
89 | ручной отправке формы методом GET.
90 | """
91 | def _initial(name, field):
92 | val = self.initial.get(name, field.initial)
93 | if not val:
94 | return val
95 | return six.text_type(val).encode('1251')
96 |
97 | fields = [(name, _initial(name, field))
98 | for name, field in list(self.fields.items())
99 | if _initial(name, field)
100 | ]
101 | params = urlencode(fields)
102 | return self.target+'?'+params
103 |
104 | def _get_signature_string(self):
105 | def _val(name):
106 | value = self.initial[name] if name in self.initial else self.fields[name].initial
107 | if value is None:
108 | return ''
109 | return six.text_type(value)
110 | standard_part = ':'.join([_val('MrchLogin'), _val('OutSum'), _val('InvId'), PASSWORD1])
111 | return self._append_extra_part(standard_part, _val)
112 |
113 |
114 | class ResultURLForm(BaseRobokassaForm):
115 | """Форма для приема результатов и проверки контрольной суммы"""
116 | OutSum = forms.CharField(max_length=15)
117 | InvId = forms.IntegerField(min_value=0)
118 | SignatureValue = forms.CharField(max_length=32)
119 |
120 | def clean(self):
121 | try:
122 | signature = self.cleaned_data['SignatureValue'].upper()
123 | if signature != self._get_signature():
124 | raise forms.ValidationError('Ошибка в контрольной сумме')
125 | except KeyError:
126 | raise forms.ValidationError('Пришли не все необходимые параметры')
127 |
128 | return self.cleaned_data
129 |
130 | def _get_signature_string(self):
131 | _val = lambda name: six.text_type(self.cleaned_data[name])
132 | standard_part = ':'.join([_val('OutSum'), _val('InvId'), PASSWORD2])
133 | return self._append_extra_part(standard_part, _val)
134 |
135 |
136 | class _RedirectPageForm(ResultURLForm):
137 | """Форма для проверки контрольной суммы на странице Success"""
138 |
139 | Culture = forms.CharField(max_length=10)
140 |
141 | def _get_signature_string(self):
142 | _val = lambda name: six.text_type(self.cleaned_data[name])
143 | standard_part = ':'.join([_val('OutSum'), _val('InvId'), PASSWORD1])
144 | return self._append_extra_part(standard_part, _val)
145 |
146 |
147 | class SuccessRedirectForm(_RedirectPageForm):
148 | """Форма для обработки страницы Success с дополнительной защитой. Она
149 | проверяет, что ROBOKASSA предварительно уведомила систему о платеже,
150 | отправив запрос на ResultURL."""
151 |
152 | def clean(self):
153 | data = super(SuccessRedirectForm, self).clean()
154 | if STRICT_CHECK:
155 | if not SuccessNotification.objects.filter(InvId=data['InvId']):
156 | raise forms.ValidationError('От ROBOKASSA не было предварительного уведомления')
157 | return data
158 |
159 |
160 | class FailRedirectForm(BaseRobokassaForm):
161 | """Форма приема результатов для перенаправления на страницу Fail"""
162 | OutSum = forms.CharField(max_length=15)
163 | InvId = forms.IntegerField(min_value=0)
164 | Culture = forms.CharField(max_length=10)
165 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ================
2 | django-robokassa3
3 | ================
4 |
5 | *Данный репозиторий является форком более неподдерживаемой библиотеки за авторством https://github.com/kmike*
6 | *Добавлены поддержка современных версий Django (1.11, 2.0), поддержка Python 3 и тестовый проект.*
7 |
8 | **django-robokassa3** - это приложение для интеграции платежной системы ROBOKASSA в
9 | проекты на Django.
10 |
11 | До использования следует ознакомиться с официальной документацией
12 | ROBOKASSA (http://docs.robokassa.ru/). Приложение реализует
13 | протокол взаимодействия, описанный в этом документе.
14 |
15 | Установка
16 | =========
17 |
18 | ::
19 |
20 | $ pip install django-robokassa3
21 |
22 | Потом следует добавить 'robokassa' в INSTALLED_APPS и выполнить ::
23 |
24 | $ python manage.py migrate
25 |
26 | Для работы требуется Django 1.11 или 2.0. Поддерживаются Python 2.7 и >=3.4.
27 |
28 | Настройка
29 | =========
30 |
31 | В settings.py нужно указать следующие настройки:
32 |
33 | * ROBOKASSA_LOGIN - логин
34 | * ROBOKASSA_PASSWORD1 - пароль №1
35 |
36 | Необязательные параметры:
37 |
38 | * ROBOKASSA_PASSWORD2 - пароль №2. Его можно не указывать, если
39 | django-robokassa используется только для вывода формы платежа.
40 | Если django-robokassa используется для приема платежей, то этот
41 | параметр обязательный.
42 |
43 | * ROBOKASSA_USE_POST - используется ли метод POST при приеме результатов от
44 | ROBOKASSA. По умолчанию - True. Считается, что для Result URL, Success URL и
45 | Fail URL выбран один и тот же метод.
46 |
47 | * ROBOKASSA_STRICT_CHECK - использовать ли строгую проверку (требовать
48 | предварительного уведомления на ResultURL). По умолчанию - True.
49 |
50 | * ROBOKASSA_TEST_MODE - включен ли тестовый режим. По умолчанию False
51 | (т.е. включен боевой режим).
52 |
53 | * ROBOKASSA_EXTRA_PARAMS - список (list) названий дополнительных параметров,
54 | которые будут передаваться вместе с запросами. "Shp" к ним приписывать не
55 | нужно.
56 |
57 | * ROBOKASSA_TEST_FORM_TARGET - url робокассы для тестового режима.
58 | Настройка предназначена для случая, когда в распоряжении не имеется
59 | доступного в интернете домена (например разработка на localhost)
60 | и вместо сервера робокассы необходимо
61 | использовать свой.
62 |
63 |
64 | Использование
65 | =============
66 |
67 | Форма для приема платежей
68 | -------------------------
69 |
70 | Для того, чтобы упростить конструирование html-форм для отправки пользователей в
71 | Robokassa, в django-robokassa есть форма RobokassaForm. Она нужна
72 | для упрощения вывода информации в шаблонах, вычисления контрольной суммы и
73 | формирования параметров GET-запросов.
74 |
75 | Пример::
76 |
77 | # views.py
78 |
79 | from django.shortcuts import get_object_or_404, render
80 | from django.contrib.auth.decorators import login_required
81 |
82 | from robokassa.forms import RobokassaForm
83 |
84 | @login_required
85 | def pay_with_robokassa(request, order_id):
86 | order = get_object_or_404(Order, pk=order_id)
87 |
88 | form = RobokassaForm(initial={
89 | 'OutSum': order.total,
90 | 'InvId': order.id,
91 | 'Desc': order.name,
92 | 'Email': request.user.email,
93 | # 'IncCurrLabel': '',
94 | # 'Culture': 'ru'
95 | })
96 |
97 | return render(request, 'pay_with_robokassa.html', {'form': form})
98 |
99 | В initial все параметры необязательны. Детальную справку по параметрам
100 | лучше посмотреть в `документации `_
101 | к Robokassa. Можно передавать в initial значения "пользовательских параметров",
102 | описанных в ROBOKASSA_EXTRA_PARAMS ('shp' к ним приписывать опять не нужно).
103 |
104 | Соответствующий шаблон::
105 |
106 | {% extends 'base.html' %}
107 |
108 | {% block content %}
109 |
113 | {% endblock %}
114 |
115 | Форма выведется в виде набора скрытых input-тегов.
116 |
117 | У формы есть атрибут target, содержащий URL, по которому форму следует
118 | отправлять. В тестовом режиме это будет тестовый URL, в боевом - боевой.
119 |
120 | Обратите внимание, {% csrf_token %} в форме не нужен (и более того, добавлять
121 | его к форме небезопасно), т.к. форма ведет на внешний сайт - сайт робокассы.
122 |
123 | Вместо отправки формы можно сформировать GET-запрос. У формы есть
124 | метод get_redirect_url, который возвращает нужный адрес со всеми параметрами.
125 | Редирект на этот адрес равносилен отправке формы методом GET.
126 |
127 | django-robokassa не включает в себя модели "Покупка" (``Order`` в примере),
128 | т.к. эта модель будет отличаться от сайта к сайту. Обработку смены статусов
129 | покупок следует осуществлять в обработчиках сигналов.
130 |
131 |
132 | Получение результатов платежей
133 | ------------------------------
134 | В Robokassa есть несколько методов определения результата платежа:
135 |
136 | 1. При переходе на страницы Success и Fail гарантируется, что платеж
137 | соответственно прошел и не прошел
138 |
139 | 2. При успешном или неудачном платеже Robokassa отправляет POST или GET запрос
140 | на Result URL.
141 |
142 | 3. Можно запрашивать статус платежа через XML-сервис.
143 |
144 | В django-robokassa на данный момент поддерживаются методы 1 и 2 и их совмещение
145 | (дополнительная проверка, что при переходе на Success URL уже было уведомление
146 | на Result URL при использовании опции ROBOKASSA_STRICT_CHECK = True).
147 |
148 | В целях безопасности лучше всегда использовать строгую проверку
149 | (с подтверждением через Result URL). Ее механизм:
150 |
151 | 1. После оплаты robokassa.ru отправляет "фоновый" запрос на ResultURL.
152 |
153 | 2. Внутри view, связанного с ResultURL, происходит проверка содержащейся в
154 | запросе md5-подписи через ROBOKASSA_PASSWORD2 (это второй пароль, который не
155 | передается по сети и известен только отправителю и получателю).
156 | ROBOKASSA_PASSWORD2 нужен для подтверждения того, что запрос был послан
157 | именно с robokassa.ru.
158 |
159 | 3. Если запрос правильный, то view шлет сигнал
160 | ``robokassa.signals.result_received``. Чтоб производить
161 | манипуляции внутри сайта (например, начислять средства согласно
162 | пришедшему запросу или менять статус заказа), нужно добавить
163 | соответствующий обработчик этого сигнала.
164 |
165 | 4. Если все в порядке, то view, связанный с Result URL,
166 | отдает robokassa.ru ответ вида ``OK``,
167 | где ```` - уникальный id текущей операции.
168 | Этот ответ необходим для того, чтобы robokassa.ru получила
169 | подтверждение того, что все необходимые действия произведены.
170 |
171 | 5. Если robokassa.ru получает этот ответ, то пользователь перенаправляется
172 | на Success URL. На этой страничке обычно лучше вывести сообщение
173 | об успешном прохождении платежа/оплаты. Если ответ view, связанной
174 | с Result URL, не соответвтует ожидаемому, то пользователь перенаправляется
175 | не на Success URL, а на Fail URL; там ему хорошо бы показать
176 | сообщение о произошедшей ошибке.
177 |
178 |
179 | Сигналы
180 | -------
181 |
182 | Обработку смены статусов покупок следует осуществлять в обработчиках сигналов.
183 |
184 | * ``robokassa.signals.result_received`` - шлется при получении уведомления от
185 | Robokassa. Получение этого сигнала означает, что оплата была успешной.
186 | В качестве sender передается экземпляр модели SuccessNotification, у
187 | которой есть атрибуты InvId и OutSum.
188 |
189 | * ``robokassa.signals.success_page_visited`` - шлется при переходе пользователя
190 | на страницу успешной оплаты. Этот сигнал следует использовать вместо
191 | result_received, если не используется строгая проверка
192 | (ROBOKASSA_STRICT_CHECK=False)
193 |
194 | * ``robokassa.signals.fail_page_visited`` - шлется при переходе пользователя
195 | на страницу ошибки оплаты. Получение этого сигнала означает, что оплата
196 | не была произведена. В обработчике следует осуществлять разблокирвку товара
197 | на складе и т.д.
198 |
199 | Все сигналы получают параметры InvId (номер заказа), OutSum (сумма оплаты) и
200 | extra (словарь с дополнительными параметрами, описанными в
201 | ROBOKASSA_EXTRA_PARAMS).
202 |
203 | Пример::
204 |
205 | from robokassa.signals import result_received
206 | from my_app.models import Order
207 |
208 | def payment_received(sender, **kwargs):
209 | order = Order.objects.get(id=kwargs['InvId'])
210 | order.status = 'paid'
211 | order.paid_sum = kwargs['OutSum']
212 | order.extra_param = kwargs['extra']['my_param']
213 | order.save()
214 |
215 | result_received.connect(payment_received)
216 |
217 |
218 |
219 | urls.py
220 | -------
221 |
222 | Для настройки Result URL, Success URL и Fail URL можно подключить
223 | модуль robokassa.urls::
224 |
225 | urlpatterns = patterns('',
226 | #...
227 | url(r'^robokassa/', include('robokassa.urls')),
228 | #...
229 | )
230 |
231 | Адреса, которые нужно указывать в панели robokassa, в этом случае будут иметь вид
232 |
233 | * Result URL: ``http://yoursite.ru/robokassa/result/``
234 | * Success URL: ``http://yoursite.ru/robokassa/success/``
235 | * Fail URL: ``http://yoursite.ru/robokassa/fail/``
236 |
237 |
238 | Шаблоны
239 | -------
240 |
241 | * ``robokassa/success.html`` - показывается в случае успешной оплаты. В
242 | контексте есть переменная form типа ``SuccessRedirectForm``, InvId
243 | и OutSum с параметрами заказа, а также все дополнительные параметры, описанные
244 | в ROBOKASSA_EXTRA_PARAMS.
245 |
246 | * ``robokassa/fail.html`` - показывается в случае неуспешной оплаты. В
247 | контексте есть переменная form типа ``FailRedirectForm``, InvId
248 | и OutSum с параметрами заказа, а также все дополнительные параметры, описанные
249 | в ROBOKASSA_EXTRA_PARAMS.
250 |
251 | * ``robokassa/error.html`` - показывается при ошибочном запросе к странице
252 | "успех" или "неудача" (например, при ошибке в контрольной сумме). В контексте
253 | есть переменная form класса ``FailRedirectForm`` или ``SuccessRedirectForm``.
254 |
255 | Разработка
256 | ==========
257 |
258 | Оригинальная более неподдерживаемая версия библиотеки: https://github.com/kmike/django-robokassa
259 | Разработка ведётся на GitHub: https://github.com/mpyrev/django-robokassa
260 |
261 | Пожелания, идеи, баг-репорты и тд. пишите в трекер: https://github.com/mpyrev/django-robokassa/issues
262 |
263 | Лицензия - MIT.
264 |
265 | Тестирование
266 | ------------
267 |
268 | Для запуска тестов установите `tox `_, склонируйте репозиторий
269 | и выполните команду
270 |
271 | ::
272 |
273 | $ tox
274 |
275 | из корня репозитория.
276 |
--------------------------------------------------------------------------------