test robokassa

prod
Dmitriy Shesterkin 9 years ago
parent d4bc5b7621
commit f09b5408ae
  1. 7
      src/customer/admin.py
  2. 30
      src/customer/migrations/0006_payment.py
  3. 19
      src/customer/migrations/0007_payment_status.py
  4. 62
      src/customer/models.py
  5. 12
      src/customer/urls.py
  6. 34
      src/customer/views/license.py
  7. 35
      src/dokumentor/settings/common.py
  8. 7
      src/dokumentor/settings/stage.py
  9. 1
      src/dokumentor/urls.py
  10. 30
      src/robokassa/__init__.py
  11. 9
      src/robokassa/apps.py
  12. 25
      src/robokassa/conf.py
  13. 199
      src/robokassa/forms.py
  14. 27
      src/robokassa/migrations/0001_initial.py
  15. 0
      src/robokassa/migrations/__init__.py
  16. 22
      src/robokassa/models.py
  17. 6
      src/robokassa/signals.py
  18. 214
      src/robokassa/tests.py
  19. 15
      src/robokassa/urls.py
  20. 99
      src/robokassa/views.py
  21. 6
      templates/robokassa/error.html
  22. 4
      templates/robokassa/fail.html
  23. 7
      templates/robokassa/form.html
  24. 4
      templates/robokassa/success.html

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.contrib import admin from django.contrib import admin
from django.utils import timezone from django.utils import timezone
from robokassa.models import SuccessNotification
from customer import forms from customer import forms
from customer import models from customer import models
@ -101,7 +102,13 @@ class ClientAdmin(admin.ModelAdmin):
] ]
@admin.register(models.Payment)
class PaymentAdmin(admin.ModelAdmin):
pass
admin.site.register(models.UserProfile, UserProfileAdmin) admin.site.register(models.UserProfile, UserProfileAdmin)
admin.site.register(models.BankAccount, BankAccountAdmin) admin.site.register(models.BankAccount, BankAccountAdmin)
admin.site.register(models.Client, ClientAdmin) admin.site.register(models.Client, ClientAdmin)
admin.site.register(models.LicensePrice) admin.site.register(models.LicensePrice)
admin.site.register(SuccessNotification)

@ -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, 'Отклонен')]),
),
]

@ -4,7 +4,6 @@ import os
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from django.utils import timezone
from PIL import Image from PIL import Image
from pytils import numeral from pytils import numeral
@ -13,11 +12,14 @@ from dateutil.relativedelta import relativedelta
from django.db import models from django.db import models
from django.db.models import Max from django.db.models import Max
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils import timezone
from django.utils.deconstruct import deconstructible
from django.conf import settings
from customer import consts, managers, utils from customer import consts, managers, utils
from myauth.models import DokUser from myauth.models import DokUser
from commons.utils import only_numerics from commons.utils import only_numerics
from django.utils.deconstruct import deconstructible from robokassa.signals import result_received
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -571,13 +573,19 @@ class License(models.Model):
numeral.choose_plural(self.pay_sum, "рубль, рубля, рублей"), numeral.choose_plural(self.pay_sum, "рубль, рубля, рублей"),
) )
def get_payment_id(self):
if self.payform == 1:
payments = Payment.objects.filter(order_number=self.id)
if payments:
return payments.first().id
# TODO: test # TODO: test
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.__prev_date and self.paid_date: if not self.__prev_date and self.paid_date:
max_date_license = License.objects.\ max_date_license = License.objects.\
filter(company=self.company).aggregate(Max('date_to'))['date_to__max'] filter(company=self.company).aggregate(Max('date_to'))['date_to__max']
today = timedelta.now().date() today = datetime.now().date()
if max_date_license < today: if max_date_license < today:
max_date_license = today - timedelta(1) max_date_license = today - timedelta(1)
self.date_from = max_date_license + relativedelta(days=1) self.date_from = max_date_license + relativedelta(days=1)
@ -604,7 +612,12 @@ class License(models.Model):
return f'<a href="{url}">Оплата безналичным платежом</a>' return f'<a href="{url}">Оплата безналичным платежом</a>'
# redirect to pay terminal with data # redirect to pay terminal with data
elif self.payform == 1: elif self.payform == 1:
return f'<a href="#">Оплата банковской картой</a>' if self.get_payment_id():
url = reverse('payment_robokassa',
kwargs={'payment_id': self.get_payment_id()})
else:
url = '#'
return f'<a href="{url}">Оплата банковской картой</a>'
elif self.status in [1, 2]: elif self.status in [1, 2]:
url = reverse( url = reverse(
@ -706,3 +719,44 @@ class LicensePrice(models.Model):
def __str__(self): def __str__(self):
return f'{self.term} {numeral.choose_plural(self.term, "месяц, месяца, месяцев")} ' \ return f'{self.term} {numeral.choose_plural(self.term, "месяц, месяца, месяцев")} ' \
f'({self.price} {numeral.choose_plural(self.price, "рубль, рубля, рублей")})' f'({self.price} {numeral.choose_plural(self.price, "рубль, рубля, рублей")})'
class Payment(models.Model):
PROCESSED = 0
SUCCESS = 1
FAIL = 2
CHOICES = (
(PROCESSED, 'Ожидает оплаты'),
(SUCCESS, 'Оплачен'),
(FAIL, 'Отклонен'),
)
order_amount = models.DecimalField('Сумма заказа', max_digits=15, decimal_places=2)
order_number = models.IntegerField('Номер заказа')
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
related_name='payment_user',
verbose_name='Пользователь'
)
created = models.DateTimeField('Время создания', auto_now_add=True)
status = models.PositiveSmallIntegerField(
verbose_name='Статус',
choices=CHOICES,
default=PROCESSED
)
class Meta:
verbose_name = 'Платеж'
verbose_name_plural = 'Платежи'
def __str__(self):
return f'{self.user}-{self.order_number}'
def payment_received(sender, **kwargs):
print(kwargs)
result_received.connect(payment_received)

@ -17,9 +17,6 @@ urlpatterns = [
url(r'^license/$', license.order_license, name='customer_order_license'), url(r'^license/$', license.order_license, name='customer_order_license'),
url(r'^delete_license/(?P<pk>\d+)/$', license.delete_license, url(r'^delete_license/(?P<pk>\d+)/$', license.delete_license,
name='customer_delete_license'), name='customer_delete_license'),
# for delete
# url(r'^get_doc/(?P<order_num>\d+)/$', documents.get_doc,
# name='customer_license_get_doc'),
url( url(
r'^get_payment_account/(?P<order_num>\d+)/$', r'^get_payment_account/(?P<order_num>\d+)/$',
@ -31,13 +28,16 @@ urlpatterns = [
documents.create_certificate_of_completion, documents.create_certificate_of_completion,
name='get_certificate_of_completion' name='get_certificate_of_completion'
), ),
url(
r'^payment/confirm/(?P<payment_id>\d+)$',
license.pay_with_robokassa,
name='payment_robokassa'
),
# for delete # for delete
# url(r'^payment/result/$', license.payment_result, name='yamoney_result'), # url(r'^payment/result/$', license.payment_result, name='yamoney_result'),
# url(r'^payment/success/$', license.payment_success, name='yamoney_success'), # url(r'^payment/success/$', license.payment_success, name='yamoney_success'),
# url(r'^payment/fail/$', license.payment_fail, name='yamoney_fail'), # url(r'^payment/fail/$', license.payment_fail, name='yamoney_fail'),
# for delete
# url(r'^license_list/$', license.license_list, name='customer_license_list'),
# url(r'^paid_list/$', license.paid_list, name='customer_paid_list'),
# for delete end # for delete end
url(r'^orders/$', license.orders_list, name='customer-orders'), url(r'^orders/$', license.orders_list, name='customer-orders'),

@ -5,7 +5,7 @@ import hashlib
import itertools import itertools
from django.db.models import Count from django.db.models import Count
from django.shortcuts import render, redirect from django.shortcuts import render, redirect, get_object_or_404
from django.http import (HttpResponseForbidden, HttpResponse, HttpResponseBadRequest) from django.http import (HttpResponseForbidden, HttpResponse, HttpResponseBadRequest)
from django.conf import settings from django.conf import settings
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@ -13,8 +13,9 @@ from django.contrib.auth.decorators import login_required
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import csrf_protect
from robokassa.forms import RobokassaForm
from customer.models import License, LicensePrice from customer.models import License, LicensePrice, Payment
from customer.forms import LicenseForm from customer.forms import LicenseForm
from customer.utils import raise_if_no_profile from customer.utils import raise_if_no_profile
@ -41,7 +42,13 @@ def order_license(request):
pay_sum=form.cleaned_data['term'].price pay_sum=form.cleaned_data['term'].price
) )
new_license.save() new_license.save()
if form.cleaned_data['payform'] == '1':
payment = Payment.objects.create(
order_amount=form.cleaned_data['term'].price,
order_number=new_license.id,
user=request.user
)
return redirect(reverse('payment_robokassa', kwargs={'payment_id': payment.id}))
return redirect(reverse('customer-orders')) return redirect(reverse('customer-orders'))
return render(request, template_name, dictionary) return render(request, template_name, dictionary)
@ -166,3 +173,24 @@ def orders_list(request):
'object_list': merged 'object_list': merged
} }
return render(request, template_name, context) return render(request, template_name, context)
@login_required
def pay_with_robokassa(request, payment_id):
payment = get_object_or_404(Payment, pk=payment_id)
# https://auth.robokassa.ru/Merchant/Index.aspx?isTest=1&MerchantLogin=Dokumentor&InvId=231849535&OutSum=100.00&SignatureValue=5c330dbb455ab540fb5fb4ee6b476f3a&Culture=ru
# http://127.0.0.1:8000/robokassa/success/?inv_id=3&InvId=3&out_summ=200.00&OutSum=200.00&crc=2c5050aaff3788c19cbd50ac6974f650&SignatureValue=2c5050aaff3788c19cbd50ac6974f650&Culture=ru&IsTest=1
# http://127.0.0.1:8000/robokassa/success/?inv_id=5&InvId=5&out_summ=3000.00&OutSum=3000.00&crc=3e9ae74938ad9397053bf184732e8bfa&SignatureValue=3e9ae74938ad9397053bf184732e8bfa&Culture=ru&IsTest=1
# 29CE5970A619428DE1E800A49E68E13B
# 3e9ae74938ad9397053bf184732e8bfa
form = RobokassaForm(initial={
'OutSum': payment.order_amount,
'InvId': payment.id,
'Desc': 'Оплата лицензии Dokumentor.ru',
'Email': payment.user.email,
'Culture': 'ru'
})
return render(request, 'robokassa/form.html', {'form': form})

@ -163,6 +163,7 @@ INSTALLED_APPS = [
'treebeard', 'treebeard',
'djangocms_text_ckeditor', 'djangocms_text_ckeditor',
'raven.contrib.django.raven_compat', 'raven.contrib.django.raven_compat',
'robokassa',
] ]
LOCAL_APPS = [ LOCAL_APPS = [
@ -204,18 +205,18 @@ LANGUAGES = (
CMS_TEMPLATES = ( CMS_TEMPLATES = (
('pages/index.html', 'Index Page'), ('pages/index.html', 'Index Page'),
('pages/inner_page.html', u'Внутренняя страница'), ('pages/inner_page.html', 'Внутренняя страница'),
('pages/placeholders.html', u'Блоки для других страниц'), ('pages/placeholders.html', 'Блоки для других страниц'),
) )
CMS_PLACEHOLDER_CONF = { CMS_PLACEHOLDER_CONF = {
'why_register': { 'why_register': {
'plugins': ['CMSDescTextBlockPlugin'], 'plugins': ['CMSDescTextBlockPlugin'],
'name': u'Зачем регистрироваться', 'name': 'Зачем регистрироваться',
}, },
'reasons': { 'reasons': {
'plugins': ['CMSExtendedTextBlockPlugin'], 'plugins': ['CMSExtendedTextBlockPlugin'],
'name': u'На главной - причины', 'name': 'На главной - причины',
}, },
} }
@ -231,7 +232,7 @@ CELERY_TIMEZONE = 'Europe/Moscow'
CALLBACK_SETTINGS = { CALLBACK_SETTINGS = {
'EMAIL_SENDER': e.get('CALLBACK_EMAIL_SENDER'), 'EMAIL_SENDER': e.get('CALLBACK_EMAIL_SENDER'),
'MANAGERS_EMAILS': e.get('CALLBACK_MANAGERS_EMAILS'), 'MANAGERS_EMAILS': e.get('CALLBACK_MANAGERS_EMAILS'),
'NEW_REQ_AVAIL_EMAIL_SUBJ': u'Вопрос техподдержке', 'NEW_REQ_AVAIL_EMAIL_SUBJ': 'Вопрос техподдержке',
} }
CELERYBEAT_SCHEDULE = { CELERYBEAT_SCHEDULE = {
@ -280,14 +281,6 @@ THUMBNAIL_PROCESSORS = (
'easy_thumbnails.processors.filters' 'easy_thumbnails.processors.filters'
) )
RAVEN_CONFIG = {
'dsn': 'http://02d524ef0d044bdfae0b39546b752cb2:'
'1e025305594d4532ae93125372dcde50@sentry.mitri4.pro/1',
# If you are using git, you can also automatically configure the
# release based on the git info.
'release': raven.fetch_git_sha(ROOT_DIR),
}
# cache settings # cache settings
COMMON_CACHE_PREFIX = 'dokumentor_' COMMON_CACHE_PREFIX = 'dokumentor_'
CMS_CACHE_PREFIX = '%scms-' % COMMON_CACHE_PREFIX CMS_CACHE_PREFIX = '%scms-' % COMMON_CACHE_PREFIX
@ -321,12 +314,26 @@ OWNER = {
'CORR_ACC': '30101810600000000786', 'CORR_ACC': '30101810600000000786',
'POSITION_BOSS': 'Руководитель организации', 'POSITION_BOSS': 'Руководитель организации',
'BOSS': 'Костенко А.Ю', 'BOSS': 'Костенко А.Ю',
'GB': 'Костенко Д', 'GB': 'Костенко А',
'STAMP': os.path.join(ROOT_DIR, 'extra', 'stamp.png'), 'STAMP': os.path.join(ROOT_DIR, 'extra', 'stamp.png'),
'SIGN_BOSS': os.path.join(ROOT_DIR, 'extra', 'boss_sign.png'), 'SIGN_BOSS': os.path.join(ROOT_DIR, 'extra', 'boss_sign.png'),
'SIGN_GB': os.path.join(ROOT_DIR, 'extra', 'gb_sign.png') 'SIGN_GB': os.path.join(ROOT_DIR, 'extra', 'gb_sign.png')
} }
# ROBOKASSA_PASSWORD1 = 'O9ohc2uT8T9s1fKwXbuq'
# ROBOKASSA_PASSWORD2 = 'D552KBeAJhhVOfKEqT62'
# Robokassa
ROBOKASSA_LOGIN = 'Dokumentor'
# Test
ROBOKASSA_PASSWORD1 = 'ZtcV4jzgJ5qI2Cx7Rc4a'
ROBOKASSA_PASSWORD2 = 'iuJI7adaUGGE96QKz17a'
ROBOKASSA_USE_POST = False
ROBOKASSA_STRICT_CHECK = True
ROBOKASSA_TEST_MODE = True
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': True, 'disable_existing_loggers': True,

@ -21,6 +21,13 @@ DATABASES = {
'default': dj_database_url.parse('postgres://dokumentor:dokumentor@db:5432/dokumentor'), 'default': dj_database_url.parse('postgres://dokumentor:dokumentor@db:5432/dokumentor'),
} }
RAVEN_CONFIG = {
'dsn': 'http://02d524ef0d044bdfae0b39546b752cb2:'
'1e025305594d4532ae93125372dcde50@sentry.mitri4.pro/1',
# If you are using git, you can also automatically configure the
# release based on the git info.
'release': raven.fetch_git_sha(ROOT_DIR),
}
DEFAULT_FROM_EMAIL = 'Открытые технологии <no-reply@o-tech.io>' DEFAULT_FROM_EMAIL = 'Открытые технологии <no-reply@o-tech.io>'
SERVER_EMAIL = DEFAULT_FROM_EMAIL SERVER_EMAIL = DEFAULT_FROM_EMAIL

@ -29,6 +29,7 @@ urlpatterns = [
url(r'^user/', include('myauth.urls')), url(r'^user/', include('myauth.urls')),
url(r'^captcha/', include('captcha.urls')), url(r'^captcha/', include('captcha.urls')),
url(r'^robokassa/', include('robokassa.urls')),
url(r'^', include('cms.urls')), url(r'^', include('cms.urls')),
] ]

@ -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…
Cancel
Save