parent
d4bc5b7621
commit
f09b5408ae
24 changed files with 847 additions and 27 deletions
@ -0,0 +1,30 @@ |
|||||||
|
# -*- coding: utf-8 -*- |
||||||
|
from __future__ import unicode_literals |
||||||
|
|
||||||
|
from django.db import migrations, models |
||||||
|
from django.conf import settings |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||||
|
('customer', '0005_auto_20170629_1224'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.CreateModel( |
||||||
|
name='Payment', |
||||||
|
fields=[ |
||||||
|
('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), |
||||||
|
('order_amount', models.DecimalField(verbose_name='Сумма заказа', max_digits=15, decimal_places=2)), |
||||||
|
('order_number', models.IntegerField(verbose_name='Номер заказа')), |
||||||
|
('created', models.DateTimeField(verbose_name='Время создания', auto_now_add=True)), |
||||||
|
('user', models.ForeignKey(verbose_name='Пользователь', related_name='payment_user', to=settings.AUTH_USER_MODEL)), |
||||||
|
], |
||||||
|
options={ |
||||||
|
'verbose_name': 'Платеж', |
||||||
|
'verbose_name_plural': 'Платежи', |
||||||
|
}, |
||||||
|
), |
||||||
|
] |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
# -*- coding: utf-8 -*- |
||||||
|
from __future__ import unicode_literals |
||||||
|
|
||||||
|
from django.db import migrations, models |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
('customer', '0006_payment'), |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.AddField( |
||||||
|
model_name='payment', |
||||||
|
name='status', |
||||||
|
field=models.PositiveSmallIntegerField(verbose_name='Статус', default=0, choices=[(0, 'Ожидает оплаты'), (1, 'Оплачен'), (2, 'Отклонен')]), |
||||||
|
), |
||||||
|
] |
||||||
@ -0,0 +1,30 @@ |
|||||||
|
import sys |
||||||
|
|
||||||
|
|
||||||
|
VERSION = (1, 3, 3) |
||||||
|
|
||||||
|
|
||||||
|
def get_version(): |
||||||
|
return '.'.join(map(str, VERSION)) |
||||||
|
|
||||||
|
|
||||||
|
PY2 = sys.version_info[0] == 2 |
||||||
|
PY3 = sys.version_info[0] == 3 |
||||||
|
|
||||||
|
|
||||||
|
def to_unicode(value): |
||||||
|
try: |
||||||
|
return unicode(value) |
||||||
|
except NameError: |
||||||
|
return str(value) |
||||||
|
|
||||||
|
|
||||||
|
def python_2_unicode_compatible(klass): |
||||||
|
if PY2: |
||||||
|
if '__str__' not in klass.__dict__: |
||||||
|
raise ValueError("@python_2_unicode_compatible cannot be applied " |
||||||
|
"to %s because it doesn't define __str__()." % |
||||||
|
klass.__name__) |
||||||
|
klass.__unicode__ = klass.__str__ |
||||||
|
klass.__str__ = lambda self: self.__unicode__().encode('utf-8') |
||||||
|
return klass |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
# -*- encoding: utf-8 -*- |
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _ |
||||||
|
from django.apps import AppConfig |
||||||
|
|
||||||
|
|
||||||
|
class DBMailConfig(AppConfig): |
||||||
|
name = 'robokassa' |
||||||
|
verbose_name = _(u'Робокасса') |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
# coding: utf-8 |
||||||
|
|
||||||
|
from django.conf import settings |
||||||
|
|
||||||
|
# обязательные параметры - реквизиты магазина |
||||||
|
LOGIN = settings.ROBOKASSA_LOGIN |
||||||
|
PASSWORD1 = settings.ROBOKASSA_PASSWORD1 |
||||||
|
PASSWORD2 = getattr(settings, 'ROBOKASSA_PASSWORD2', None) |
||||||
|
|
||||||
|
# использовать ли метод POST при приеме результатов |
||||||
|
USE_POST = getattr(settings, 'ROBOKASSA_USE_POST', True) |
||||||
|
|
||||||
|
# требовать предварительного уведомления на ResultURL |
||||||
|
STRICT_CHECK = getattr(settings, 'ROBOKASSA_STRICT_CHECK', True) |
||||||
|
|
||||||
|
# тестовый режим |
||||||
|
TEST_MODE = getattr(settings, 'ROBOKASSA_TEST_MODE', False) |
||||||
|
|
||||||
|
# url, по которому будет идти отправка форм |
||||||
|
FORM_TARGET = 'https://merchant.roboxchange.com/Index.aspx' |
||||||
|
if TEST_MODE: |
||||||
|
FORM_TARGET = 'https://auth.robokassa.ru/Merchant/Index.aspx' |
||||||
|
|
||||||
|
# список пользовательских параметров ("shp" к ним приписывать не нужно) |
||||||
|
EXTRA_PARAMS = sorted(getattr(settings, 'ROBOKASSA_EXTRA_PARAMS', [])) |
||||||
@ -0,0 +1,199 @@ |
|||||||
|
# coding: utf-8 |
||||||
|
|
||||||
|
from hashlib import md5 |
||||||
|
from django import forms |
||||||
|
|
||||||
|
try: |
||||||
|
from urllib import urlencode |
||||||
|
except ImportError: |
||||||
|
from urllib.parse import urlencode |
||||||
|
|
||||||
|
from robokassa.conf import LOGIN, PASSWORD1, PASSWORD2 |
||||||
|
from robokassa.conf import STRICT_CHECK, FORM_TARGET, EXTRA_PARAMS, TEST_MODE |
||||||
|
from robokassa.models import SuccessNotification |
||||||
|
from robokassa import to_unicode, PY2 |
||||||
|
|
||||||
|
|
||||||
|
class BaseRobokassaForm(forms.Form): |
||||||
|
def __init__(self, *args, **kwargs): |
||||||
|
self.password1 = kwargs.pop('password1', PASSWORD1) |
||||||
|
self.password2 = kwargs.pop('password2', PASSWORD2) |
||||||
|
self.login = kwargs.pop('login', LOGIN) |
||||||
|
|
||||||
|
super(BaseRobokassaForm, self).__init__(*args, **kwargs) |
||||||
|
|
||||||
|
# создаем дополнительные поля |
||||||
|
for key in EXTRA_PARAMS: |
||||||
|
self.fields['shp' + key] = forms.CharField(required=False) |
||||||
|
if 'initial' in kwargs: |
||||||
|
self.fields['shp' + key].initial = kwargs['initial'].get( |
||||||
|
key, 'None') |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def _append_extra_part(standard_part, value_func): |
||||||
|
extra_part = ":".join( |
||||||
|
["%s=%s" % ('shp' + key, value_func('shp' + key)) |
||||||
|
for key in EXTRA_PARAMS]) |
||||||
|
if extra_part: |
||||||
|
return ':'.join([standard_part, extra_part]) |
||||||
|
return standard_part |
||||||
|
|
||||||
|
def extra_params(self): |
||||||
|
extra = {} |
||||||
|
for param in EXTRA_PARAMS: |
||||||
|
if ('shp' + param) in self.cleaned_data: |
||||||
|
extra[param] = self.cleaned_data['shp' + param] |
||||||
|
return extra |
||||||
|
|
||||||
|
def _get_signature(self): |
||||||
|
if PY2: |
||||||
|
return md5(self._get_signature_string()).hexdigest().upper() |
||||||
|
return md5( |
||||||
|
self._get_signature_string().encode("ascii")).hexdigest().upper() |
||||||
|
|
||||||
|
def _get_signature_string(self): |
||||||
|
raise NotImplementedError |
||||||
|
|
||||||
|
|
||||||
|
class RobokassaForm(BaseRobokassaForm): |
||||||
|
# login магазина в обменном пункте |
||||||
|
MrchLogin = forms.CharField(max_length=20) |
||||||
|
|
||||||
|
# требуемая к получению сумма |
||||||
|
OutSum = forms.DecimalField( |
||||||
|
min_value=0, max_digits=20, decimal_places=2, required=False) |
||||||
|
|
||||||
|
# номер счета в магазине (должен быть уникальным для магазина) |
||||||
|
InvId = forms.IntegerField(min_value=0, required=False) |
||||||
|
|
||||||
|
# описание покупки |
||||||
|
Desc = forms.CharField(max_length=100, required=False) |
||||||
|
|
||||||
|
# контрольная сумма MD5 |
||||||
|
SignatureValue = forms.CharField(max_length=32) |
||||||
|
|
||||||
|
# предлагаемая валюта платежа |
||||||
|
IncCurrLabel = forms.CharField(max_length=10, required=False) |
||||||
|
|
||||||
|
# e-mail пользователя |
||||||
|
Email = forms.CharField(max_length=100, required=False) |
||||||
|
|
||||||
|
# язык общения с клиентом (en или ru) |
||||||
|
Culture = forms.CharField(max_length=10, required=False) |
||||||
|
if TEST_MODE: |
||||||
|
IsTest = forms.IntegerField(min_value=1, required=False) |
||||||
|
|
||||||
|
# Параметр с URL'ом, на который форма должны быть отправлена. |
||||||
|
# Может пригодиться для использования в шаблоне. |
||||||
|
target = FORM_TARGET |
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs): |
||||||
|
super(RobokassaForm, self).__init__(*args, **kwargs) |
||||||
|
|
||||||
|
# скрытый виджет по умолчанию |
||||||
|
for field in self.fields: |
||||||
|
self.fields[field].widget = forms.HiddenInput() |
||||||
|
|
||||||
|
self.fields['MrchLogin'].initial = self.login |
||||||
|
self.fields['SignatureValue'].initial = self._get_signature() |
||||||
|
if TEST_MODE: |
||||||
|
self.fields['IsTest'].initial = 1 |
||||||
|
|
||||||
|
def get_redirect_url(self): |
||||||
|
""" |
||||||
|
Получить URL с GET-параметрами, соответствующими значениям полей в |
||||||
|
форме. Редирект на адрес, возвращаемый этим методом, эквивалентен |
||||||
|
ручной отправке формы методом GET. |
||||||
|
""" |
||||||
|
|
||||||
|
def _initial(key, fld): |
||||||
|
val = self.initial.get(key, fld.initial) |
||||||
|
if not val: |
||||||
|
return val |
||||||
|
return to_unicode(val).encode('1251') |
||||||
|
|
||||||
|
fields = [ |
||||||
|
(name, _initial(name, field)) |
||||||
|
for name, field in self.fields.items() |
||||||
|
if _initial(name, field) |
||||||
|
] |
||||||
|
return self.target + '?' + urlencode(fields) |
||||||
|
|
||||||
|
def _get_signature_string(self): |
||||||
|
def _val(name): |
||||||
|
if name in self.initial: |
||||||
|
value = self.initial[name] |
||||||
|
else: |
||||||
|
value = self.fields[name].initial |
||||||
|
return '' if value is None else to_unicode(value) |
||||||
|
|
||||||
|
standard_part = ':'.join( |
||||||
|
[_val('MrchLogin'), _val('OutSum'), _val('InvId'), self.password1]) |
||||||
|
return self._append_extra_part(standard_part, _val) |
||||||
|
|
||||||
|
|
||||||
|
class ResultURLForm(BaseRobokassaForm): |
||||||
|
""" |
||||||
|
Форма для приема результатов и проверки контрольной суммы. |
||||||
|
""" |
||||||
|
|
||||||
|
OutSum = forms.CharField(max_length=15) |
||||||
|
InvId = forms.IntegerField(min_value=0) |
||||||
|
SignatureValue = forms.CharField(max_length=32) |
||||||
|
|
||||||
|
def clean(self): |
||||||
|
try: |
||||||
|
signature = self.cleaned_data['SignatureValue'].upper() |
||||||
|
if signature != self._get_signature(): |
||||||
|
raise forms.ValidationError(u'Ошибка в контрольной сумме') |
||||||
|
except KeyError: |
||||||
|
raise forms.ValidationError(u'Пришли не все необходимые параметры') |
||||||
|
|
||||||
|
return self.cleaned_data |
||||||
|
|
||||||
|
def _get_signature_string(self): |
||||||
|
_val = lambda name: to_unicode(self.cleaned_data[name]) |
||||||
|
standard_part = ':'.join( |
||||||
|
[_val('OutSum'), _val('InvId'), self.password2]) |
||||||
|
return self._append_extra_part(standard_part, _val) |
||||||
|
|
||||||
|
|
||||||
|
class _RedirectPageForm(ResultURLForm): |
||||||
|
""" |
||||||
|
Форма для проверки контрольной суммы на странице Success. |
||||||
|
""" |
||||||
|
|
||||||
|
Culture = forms.CharField(max_length=10) |
||||||
|
|
||||||
|
def _get_signature_string(self): |
||||||
|
_val = lambda name: to_unicode(self.cleaned_data[name]) |
||||||
|
standard_part = ':'.join( |
||||||
|
[_val('OutSum'), _val('InvId'), self.password1]) |
||||||
|
return self._append_extra_part(standard_part, _val) |
||||||
|
|
||||||
|
|
||||||
|
class SuccessRedirectForm(_RedirectPageForm): |
||||||
|
""" |
||||||
|
Форма для обработки страницы Success с дополнительной защитой. Она |
||||||
|
проверяет, что ROBOKASSA предварительно уведомила систему о платеже, |
||||||
|
отправив запрос на ResultURL. |
||||||
|
""" |
||||||
|
|
||||||
|
def clean(self): |
||||||
|
data = super(SuccessRedirectForm, self).clean() |
||||||
|
if STRICT_CHECK: |
||||||
|
if not SuccessNotification.objects.filter(InvId=data['InvId']): |
||||||
|
raise forms.ValidationError( |
||||||
|
'От ROBOKASSA не было предварительного уведомления' |
||||||
|
) |
||||||
|
return data |
||||||
|
|
||||||
|
|
||||||
|
class FailRedirectForm(BaseRobokassaForm): |
||||||
|
""" |
||||||
|
Форма приема результатов для перенаправления на страницу Fail. |
||||||
|
""" |
||||||
|
|
||||||
|
OutSum = forms.CharField(max_length=15) |
||||||
|
InvId = forms.IntegerField(min_value=0) |
||||||
|
Culture = forms.CharField(max_length=10) |
||||||
@ -0,0 +1,27 @@ |
|||||||
|
# -*- coding: utf-8 -*- |
||||||
|
from __future__ import unicode_literals |
||||||
|
|
||||||
|
from django.db import models, migrations |
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration): |
||||||
|
|
||||||
|
dependencies = [ |
||||||
|
] |
||||||
|
|
||||||
|
operations = [ |
||||||
|
migrations.CreateModel( |
||||||
|
name='SuccessNotification', |
||||||
|
fields=[ |
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), |
||||||
|
('InvId', models.IntegerField(verbose_name='\u041d\u043e\u043c\u0435\u0440 \u0437\u0430\u043a\u0430\u0437\u0430', db_index=True)), |
||||||
|
('OutSum', models.CharField(max_length=15, verbose_name='\u0421\u0443\u043c\u043c\u0430')), |
||||||
|
('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')), |
||||||
|
], |
||||||
|
options={ |
||||||
|
'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', |
||||||
|
'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)', |
||||||
|
}, |
||||||
|
bases=(models.Model,), |
||||||
|
), |
||||||
|
] |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
# coding: utf-8 |
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _ |
||||||
|
from robokassa import python_2_unicode_compatible |
||||||
|
from django.db import models |
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible |
||||||
|
class SuccessNotification(models.Model): |
||||||
|
InvId = models.IntegerField(_(u'Номер заказа'), db_index=True) |
||||||
|
OutSum = models.CharField(_(u'Сумма'), max_length=15) |
||||||
|
|
||||||
|
created_at = models.DateTimeField( |
||||||
|
_(u'Дата и время получения уведомления'), auto_now_add=True) |
||||||
|
|
||||||
|
class Meta: |
||||||
|
verbose_name = _(u'Уведомление об успешном платеже') |
||||||
|
verbose_name_plural = _( |
||||||
|
u'Уведомления об успешных платежах (ROBOKASSA)') |
||||||
|
|
||||||
|
def __str__(self): |
||||||
|
return u'#%d: %s (%s)' % (self.InvId, self.OutSum, self.created_at) |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
# coding: utf-8 |
||||||
|
from django.dispatch import Signal |
||||||
|
|
||||||
|
result_received = Signal(providing_args=["InvId", "OutSum"]) |
||||||
|
success_page_visited = Signal(providing_args=["InvId", "OutSum"]) |
||||||
|
fail_page_visited = Signal(providing_args=["InvId", "OutSum"]) |
||||||
@ -0,0 +1,214 @@ |
|||||||
|
# coding: utf-8 |
||||||
|
|
||||||
|
from unittest import TestCase |
||||||
|
from django.test import TestCase as DjangoTestCase |
||||||
|
from robokassa.forms import RobokassaForm, ResultURLForm |
||||||
|
from robokassa.conf import LOGIN, PASSWORD1, PASSWORD2 |
||||||
|
|
||||||
|
|
||||||
|
CUSTOM_LOGIN = 'test_login2' |
||||||
|
CUSTOM_PASS1 = 'test_password21' |
||||||
|
CUSTOM_PASS2 = 'test_password22' |
||||||
|
|
||||||
|
|
||||||
|
class RobokassaFormTest(TestCase): |
||||||
|
def setUp(self): |
||||||
|
self.form = RobokassaForm( |
||||||
|
initial={ |
||||||
|
'OutSum': 100.00, |
||||||
|
'InvId': 58, |
||||||
|
'Desc': u'Холодильник "Бирюса"', |
||||||
|
'Email': 'vasia@example.com' |
||||||
|
}) |
||||||
|
|
||||||
|
def testSignature(self): |
||||||
|
self.assertEqual( |
||||||
|
self.form._get_signature_string(), |
||||||
|
'%s:100.0:58:%s:shpparam1=None:shpparam2=None' % (LOGIN, PASSWORD1) |
||||||
|
) |
||||||
|
self.assertEqual(len(self.form.fields['SignatureValue'].initial), 32) |
||||||
|
|
||||||
|
def testSignatureMissingParams(self): |
||||||
|
form = RobokassaForm(initial={'InvId': 5}) |
||||||
|
self.assertEqual( |
||||||
|
form._get_signature_string(), |
||||||
|
'%s::5:%s:shpparam1=None:shpparam2=None' % (LOGIN, PASSWORD1) |
||||||
|
) |
||||||
|
|
||||||
|
def testRedirectUrl(self): |
||||||
|
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&SignatureVa" \ |
||||||
|
"lue=0EC23BE40003640B35EC07F6615FFB57&Email=vasia%40exa" \ |
||||||
|
"mple.com&shpparam1=None&shpparam2=None" |
||||||
|
self.assertEqual(self.form.get_redirect_url(), url) |
||||||
|
|
||||||
|
|
||||||
|
class RobokassaFormExtraTest(TestCase): |
||||||
|
def testExtra(self): |
||||||
|
form = RobokassaForm( |
||||||
|
initial={ |
||||||
|
'InvId': 58, |
||||||
|
'OutSum': 100, |
||||||
|
'param1': 'value1', |
||||||
|
'param2': 'value2' |
||||||
|
}) |
||||||
|
self.assertEqual( |
||||||
|
form._get_signature_string(), |
||||||
|
'%s:100:58:%s:shpparam1=value1:shpparam2=value2' % ( |
||||||
|
LOGIN, PASSWORD1) |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class ResultURLTest(DjangoTestCase): |
||||||
|
def testFormExtra(self): |
||||||
|
form = ResultURLForm({ |
||||||
|
'OutSum': '100', |
||||||
|
'InvId': '58', |
||||||
|
'SignatureValue': 'B2111A06F6B7A1E090D38367BF7032D9', |
||||||
|
'shpparam1': 'Vasia', |
||||||
|
'shpparam2': 'None', |
||||||
|
}) |
||||||
|
self.assertTrue(form.is_valid()) |
||||||
|
self.assertEqual( |
||||||
|
form._get_signature_string(), |
||||||
|
'100:58:%s:shpparam1=Vasia:shpparam2=None' % PASSWORD2) |
||||||
|
self.assertEqual( |
||||||
|
form.extra_params(), {'param1': 'Vasia', 'param2': 'None'} |
||||||
|
) |
||||||
|
|
||||||
|
def testFormValid(self): |
||||||
|
self.assertTrue(ResultURLForm({ |
||||||
|
'OutSum': '100', |
||||||
|
'InvId': '58', |
||||||
|
'SignatureValue': '877D3BAF8381F70E56638C3BC82580C5', |
||||||
|
'shpparam1': 'None', |
||||||
|
'shpparam2': 'None', |
||||||
|
}).is_valid()) |
||||||
|
|
||||||
|
self.assertFalse(ResultURLForm({ |
||||||
|
'OutSum': '101', |
||||||
|
'InvId': '58', |
||||||
|
'SignatureValue': '877D3BAF8381F70E56638C3BC82580C5', |
||||||
|
'shpparam1': 'None', |
||||||
|
'shpparam2': 'None', |
||||||
|
}).is_valid()) |
||||||
|
|
||||||
|
self.assertFalse(ResultURLForm({ |
||||||
|
'OutSum': '100', |
||||||
|
'InvId': '58', |
||||||
|
'SignatureValue': '877D3BAF8381F70E56638C3BC82580C5', |
||||||
|
'shpparam1': 'Vasia', |
||||||
|
'shpparam2': 'None', |
||||||
|
}).is_valid()) |
||||||
|
|
||||||
|
def testEmptyFormValid(self): |
||||||
|
self.assertFalse(ResultURLForm().is_valid()) |
||||||
|
|
||||||
|
|
||||||
|
class RobokassaCustomCredentialFormTest(TestCase): |
||||||
|
def setUp(self): |
||||||
|
self.form = RobokassaForm( |
||||||
|
initial={ |
||||||
|
'OutSum': 100.00, |
||||||
|
'InvId': 58, |
||||||
|
'Desc': u'Холодильник "Бирюса"', |
||||||
|
'Email': 'vasia@example.com' |
||||||
|
}, |
||||||
|
login=CUSTOM_LOGIN, password1=CUSTOM_PASS1, password2=CUSTOM_PASS2 |
||||||
|
) |
||||||
|
|
||||||
|
def testSignature(self): |
||||||
|
self.assertEqual( |
||||||
|
self.form._get_signature_string(), |
||||||
|
'%s:100.0:58:%s:shpparam1=None:shpparam2=None' % ( |
||||||
|
CUSTOM_LOGIN, CUSTOM_PASS1) |
||||||
|
) |
||||||
|
self.assertEqual(len(self.form.fields['SignatureValue'].initial), 32) |
||||||
|
|
||||||
|
def testSignatureMissingParams(self): |
||||||
|
form = RobokassaForm( |
||||||
|
initial={'InvId': 5}, |
||||||
|
login=CUSTOM_LOGIN, password1=CUSTOM_PASS1, password2=CUSTOM_PASS2) |
||||||
|
self.assertEqual( |
||||||
|
form._get_signature_string(), |
||||||
|
'%s::5:%s:shpparam1=None:shpparam2=None' % ( |
||||||
|
CUSTOM_LOGIN, CUSTOM_PASS1) |
||||||
|
) |
||||||
|
|
||||||
|
def testRedirectUrl(self): |
||||||
|
url = "https://merchant.roboxchange.com/Index.aspx?MrchLogin=" \ |
||||||
|
"test_login2&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=659" \ |
||||||
|
"9E0D576E94D4E8616A40B16B8288F&Email=vasia%40example.com&sh" \ |
||||||
|
"pparam1=None&shpparam2=None" |
||||||
|
self.assertEqual(self.form.get_redirect_url(), url) |
||||||
|
|
||||||
|
|
||||||
|
class RobokassaCustomCredentialFormExtraTest(TestCase): |
||||||
|
def testExtra(self): |
||||||
|
form = RobokassaForm( |
||||||
|
initial={ |
||||||
|
'InvId': 58, |
||||||
|
'OutSum': 100, |
||||||
|
'param1': 'value1', |
||||||
|
'param2': 'value2' |
||||||
|
}, |
||||||
|
login=CUSTOM_LOGIN, password1=CUSTOM_PASS1, password2=CUSTOM_PASS2 |
||||||
|
) |
||||||
|
self.assertEqual( |
||||||
|
form._get_signature_string(), |
||||||
|
'%s:100:58:%s:shpparam1=value1:shpparam2=value2' % ( |
||||||
|
CUSTOM_LOGIN, CUSTOM_PASS1) |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class RobokassaCustomCredentialResultURLTest(DjangoTestCase): |
||||||
|
def testFormExtra(self): |
||||||
|
form = ResultURLForm({ |
||||||
|
'OutSum': '100', |
||||||
|
'InvId': '58', |
||||||
|
'SignatureValue': '8D63137C2AC89F7D5B7E6CD448DA64AF', |
||||||
|
'shpparam1': 'Vasia', |
||||||
|
'shpparam2': 'None', |
||||||
|
}, login=CUSTOM_LOGIN, password1=CUSTOM_PASS1, password2=CUSTOM_PASS2) |
||||||
|
self.assertTrue(form.is_valid()) |
||||||
|
self.assertEqual( |
||||||
|
form._get_signature_string(), |
||||||
|
'100:58:%s:shpparam1=Vasia:shpparam2=None' % CUSTOM_PASS2) |
||||||
|
self.assertEqual( |
||||||
|
form.extra_params(), {'param1': 'Vasia', 'param2': 'None'} |
||||||
|
) |
||||||
|
|
||||||
|
def testFormValid(self): |
||||||
|
self.assertTrue(ResultURLForm({ |
||||||
|
'OutSum': '100', |
||||||
|
'InvId': '58', |
||||||
|
'SignatureValue': '3D7DE0A71282E50308ED401DC24BBD5B', |
||||||
|
'shpparam1': 'None', |
||||||
|
'shpparam2': 'None', |
||||||
|
}, login=CUSTOM_LOGIN, password1=CUSTOM_PASS1, password2=CUSTOM_PASS2 |
||||||
|
).is_valid()) |
||||||
|
|
||||||
|
self.assertFalse(ResultURLForm({ |
||||||
|
'OutSum': '101', |
||||||
|
'InvId': '58', |
||||||
|
'SignatureValue': '3D7DE0A71282E50308ED401DC24BBD5B', |
||||||
|
'shpparam1': 'None', |
||||||
|
'shpparam2': 'None', |
||||||
|
}, login=CUSTOM_LOGIN, password1=CUSTOM_PASS1, password2=CUSTOM_PASS2 |
||||||
|
).is_valid()) |
||||||
|
|
||||||
|
self.assertFalse(ResultURLForm({ |
||||||
|
'OutSum': '100', |
||||||
|
'InvId': '58', |
||||||
|
'SignatureValue': '3D7DE0A71282E50308ED401DC24BBD5B', |
||||||
|
'shpparam1': 'Vasia', |
||||||
|
'shpparam2': 'None', |
||||||
|
}, login=CUSTOM_LOGIN, password1=CUSTOM_PASS1, password2=CUSTOM_PASS2 |
||||||
|
).is_valid()) |
||||||
|
|
||||||
|
def testEmptyFormValid(self): |
||||||
|
self.assertFalse(ResultURLForm( |
||||||
|
login=CUSTOM_LOGIN, password1=CUSTOM_PASS1, password2=CUSTOM_PASS2 |
||||||
|
).is_valid()) |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
# coding: utf-8 |
||||||
|
|
||||||
|
try: |
||||||
|
from django.conf.urls.defaults import url |
||||||
|
except ImportError: |
||||||
|
from django.conf.urls import url |
||||||
|
|
||||||
|
from . import views as v |
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [ |
||||||
|
url(r'^result/$', v.receive_result, name='robokassa_result'), |
||||||
|
url(r'^success/$', v.success, name='robokassa_success'), |
||||||
|
url(r'^fail/$', v.fail, name='robokassa_fail'), |
||||||
|
] |
||||||
@ -0,0 +1,99 @@ |
|||||||
|
# coding: utf-8 |
||||||
|
|
||||||
|
from django.http import HttpResponse |
||||||
|
from django.template.response import TemplateResponse |
||||||
|
from django.views.decorators.csrf import csrf_exempt |
||||||
|
|
||||||
|
from robokassa.conf import USE_POST |
||||||
|
from robokassa.forms import ( |
||||||
|
ResultURLForm, SuccessRedirectForm, FailRedirectForm) |
||||||
|
from robokassa.models import SuccessNotification |
||||||
|
from robokassa.signals import ( |
||||||
|
result_received, success_page_visited, fail_page_visited) |
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt |
||||||
|
def receive_result(request, **credentials): |
||||||
|
""" |
||||||
|
Обработчик для ResultURL |
||||||
|
""" |
||||||
|
data = request.POST if USE_POST else request.GET |
||||||
|
form = ResultURLForm(data, **credentials) |
||||||
|
print(form.is_valid()) |
||||||
|
if form.is_valid(): |
||||||
|
inv_id = form.cleaned_data['InvId'] |
||||||
|
out_sum = form.cleaned_data['OutSum'] |
||||||
|
print('----------------------------------------------------') |
||||||
|
print(form.cleaned_data['InvId'], form.cleaned_data['OutSum']) |
||||||
|
# сохраняем данные об успешном уведомлении в базе, чтобы |
||||||
|
# можно было выполнить дополнительную проверку на странице успешного |
||||||
|
# заказа |
||||||
|
notification = SuccessNotification.objects.create( |
||||||
|
InvId=inv_id, OutSum=out_sum |
||||||
|
) |
||||||
|
|
||||||
|
# дополнительные действия с заказом (например, смену его статуса) можно |
||||||
|
# осуществить в обработчике сигнала robokassa.signals.result_received |
||||||
|
result_received.send( |
||||||
|
sender=notification, InvId=inv_id, OutSum=out_sum, |
||||||
|
extra=form.extra_params()) |
||||||
|
|
||||||
|
return HttpResponse('OK%s' % inv_id) |
||||||
|
return HttpResponse('error: bad signature') |
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt |
||||||
|
def success(request, template_name='robokassa/success.html', |
||||||
|
extra_context=None, |
||||||
|
error_template_name='robokassa/error.html', **credentials): |
||||||
|
""" |
||||||
|
Обработчик для SuccessURL. |
||||||
|
""" |
||||||
|
|
||||||
|
data = request.POST if USE_POST else request.GET |
||||||
|
form = SuccessRedirectForm(data, **credentials) |
||||||
|
if form.is_valid(): |
||||||
|
inv_id = form.cleaned_data['InvId'] |
||||||
|
out_sum = form.cleaned_data['OutSum'] |
||||||
|
|
||||||
|
# в случае, когда не используется строгая проверка, действия с заказом |
||||||
|
# можно осуществлять в обработчике сигнала |
||||||
|
# robokassa.signals.success_page_visited |
||||||
|
success_page_visited.send( |
||||||
|
sender=form, InvId=inv_id, OutSum=out_sum, |
||||||
|
extra=form.extra_params()) |
||||||
|
|
||||||
|
context = {'InvId': inv_id, 'OutSum': out_sum, 'form': form} |
||||||
|
context.update(form.extra_params()) |
||||||
|
context.update(extra_context or {}) |
||||||
|
return TemplateResponse(request, template_name, context) |
||||||
|
|
||||||
|
return TemplateResponse(request, error_template_name, {'form': form}) |
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt |
||||||
|
def fail(request, template_name='robokassa/fail.html', extra_context=None, |
||||||
|
error_template_name='robokassa/error.html', **credentials): |
||||||
|
""" |
||||||
|
Обработчик для FailURL. |
||||||
|
""" |
||||||
|
|
||||||
|
data = request.POST if USE_POST else request.GET |
||||||
|
form = FailRedirectForm(data, **credentials) |
||||||
|
if form.is_valid(): |
||||||
|
inv_id = form.cleaned_data['InvId'] |
||||||
|
out_sum = form.cleaned_data['OutSum'] |
||||||
|
|
||||||
|
# дополнительные действия с заказом (например, смену его статуса для |
||||||
|
# разблокировки товара на складе) можно осуществить в обработчике |
||||||
|
# сигнала robokassa.signals.fail_page_visited |
||||||
|
fail_page_visited.send( |
||||||
|
sender=form, InvId=inv_id, OutSum=out_sum, |
||||||
|
extra=form.extra_params()) |
||||||
|
|
||||||
|
context = {'InvId': inv_id, 'OutSum': out_sum, 'form': form} |
||||||
|
context.update(form.extra_params()) |
||||||
|
context.update(extra_context or {}) |
||||||
|
return TemplateResponse(request, template_name, context) |
||||||
|
|
||||||
|
return TemplateResponse(request, error_template_name, {'form': form}) |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
{% block content %} |
||||||
|
Ошибка при оплате |
||||||
|
{{ form.as_p }} |
||||||
|
|
||||||
|
{% endblock content %} |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
{% block content %} |
||||||
|
Неудачная оплата |
||||||
|
{% endblock content %} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
{% block content %} |
||||||
|
<form action="{{ form.target }}" method="GET"> |
||||||
|
<p>{{ form.as_p }}</p> |
||||||
|
<p><input type="submit" value="Оплатить"></p> |
||||||
|
</form> |
||||||
|
{% endblock %} |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
{% extends 'base.html' %} |
||||||
|
{% block content %} |
||||||
|
Оплата успешна |
||||||
|
{% endblock content %} |
||||||
Loading…
Reference in new issue