├── sberbank ├── migrations │ ├── __init__.py │ ├── 0004_logentry_request_text.py │ ├── 0006_payment_method.py │ ├── 0002_auto_20180802_1820.py │ ├── 0003_auto_20180804_1932.py │ ├── 0001_initial.py │ └── 0005_auto_20180831_0901.py ├── __init__.py ├── locale │ └── ru │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── apps.py ├── tasks.py ├── urls.py ├── exceptions.py ├── serializers.py ├── util.py ├── admin.py ├── models.py ├── views.py └── service.py ├── .gitignore ├── Makefile ├── setup.py ├── LICENSE └── README.md /sberbank/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | django_sberbank.egg-info/ 3 | build/ 4 | dist/ 5 | -------------------------------------------------------------------------------- /sberbank/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'sberbank.apps.AppConfig' # noqa: invalid-name 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: dist 2 | 3 | dist: 4 | python3.5m setup.py sdist bdist_wheel 5 | 6 | upload: dist 7 | twine upload dist/* 8 | 9 | -------------------------------------------------------------------------------- /sberbank/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madprogrammer/django-sberbank/HEAD/sberbank/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /sberbank/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig as BaseAppConfig 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | 5 | class AppConfig(BaseAppConfig): 6 | name = 'sberbank' 7 | verbose_name = _('sberbank') 8 | -------------------------------------------------------------------------------- /sberbank/tasks.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | from sberbank.models import Payment, Status 4 | from sberbank.service import BankService 5 | 6 | 7 | def check_payments(): 8 | unchecked_payments = Payment.objects.filter( 9 | status=Status.PENDING, bank_id__isnull=False) 10 | for payment in unchecked_payments: 11 | BankService(settings.MERCHANT_KEY).check_status(payment.uid) 12 | -------------------------------------------------------------------------------- /sberbank/migrations/0004_logentry_request_text.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.7 on 2018-08-17 07:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('sberbank', '0003_auto_20180804_1932'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='logentry', 15 | name='request_text', 16 | field=models.TextField(blank=True, null=True, verbose_name='request text'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /sberbank/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from sberbank import views 3 | 4 | urlpatterns = [ # noqa: pylint=invalid-name 5 | url('payment/callback', views.callback), 6 | url('payment/success', views.redirect, {'kind': 'success'}), 7 | url('payment/fail', views.redirect, {'kind': 'fail'}), 8 | url('payment/status/(?P[^/]+)/', views.StatusView.as_view()), 9 | url('payment/bindings/(?P[^/]+)/', views.BindingsView.as_view()), 10 | url('payment/binding/(?P[^/]+)/', views.BindingView.as_view()), 11 | url('payment/history/(?P[^/]+)/', views.GetHistoryView.as_view()), 12 | ] 13 | -------------------------------------------------------------------------------- /sberbank/migrations/0006_payment_method.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.8 on 2018-11-18 17:06 2 | 3 | from django.db import migrations, models 4 | import sberbank.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('sberbank', '0005_auto_20180831_0901'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='payment', 16 | name='method', 17 | field=models.PositiveSmallIntegerField(choices=[(0, 'UNKNOWN'), (1, 'WEB'), (2, 'APPLE'), (3, 'GOOGLE')], db_index=True, default=sberbank.models.Method(0), verbose_name='method'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup( 4 | name='django-sberbank', 5 | version='0.2.31', 6 | description='Django app for Sberbank payments', 7 | url='http://github.com/madprogrammer/django-sberbank', 8 | author='Sergey Anufrienko', 9 | author_email='sergey.anoufrienko@gmail.com', 10 | license='BSD', 11 | packages=setuptools.find_packages(), 12 | platforms='any', 13 | zip_safe=False, 14 | install_requires=[ 15 | 'django', 16 | 'jsonfield2', 17 | 'djangorestframework', 18 | 'requests', 19 | ], 20 | classifiers=[ 21 | "Programming Language :: Python :: 3", 22 | "License :: OSI Approved :: BSD License", 23 | "Operating System :: OS Independent", 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /sberbank/exceptions.py: -------------------------------------------------------------------------------- 1 | from requests import RequestException 2 | 3 | 4 | class NetworkException(RequestException): 5 | def __init__(self, payment_id): 6 | self.payment_id = payment_id 7 | super().__init__('Network error. Payment ID {}'.format(payment_id)) 8 | 9 | 10 | class ProcessingException(RequestException): 11 | def __init__(self, payment_id, error_text=None, error_code=None): 12 | self.payment_id = payment_id 13 | self.error_text = error_text 14 | self.error_code = int(error_code) if error_code else None 15 | super().__init__('Bank error. Payment ID {}. Info: {} {}'.format( 16 | payment_id, error_text, error_code)) 17 | 18 | 19 | class PaymentNotFoundException(Exception): 20 | def __init__(self): 21 | super().__init__('Payment_id not found in DB') 22 | -------------------------------------------------------------------------------- /sberbank/migrations/0002_auto_20180802_1820.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.7 on 2018-08-02 18:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('sberbank', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='payment', 15 | name='client_id', 16 | field=models.TextField(blank=True, null=True, verbose_name='client ID'), 17 | ), 18 | migrations.AlterField( 19 | model_name='logentry', 20 | name='request_type', 21 | field=models.CharField(choices=[(0, 'CREATE'), (1, 'CALLBACK'), (2, 'CHECK_STATUS'), (3, 'REDIRECT'), (4, 'GET_BINDINGS')], db_index=True, max_length=1, verbose_name='request type'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /sberbank/migrations/0003_auto_20180804_1932.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.7 on 2018-08-04 19:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('sberbank', '0002_auto_20180802_1820'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='logentry', 15 | options={'ordering': ['-created'], 'verbose_name': 'запись в журнале', 'verbose_name_plural': 'записи в журнале'}, 16 | ), 17 | migrations.RemoveField( 18 | model_name='logentry', 19 | name='request_type', 20 | ), 21 | migrations.AddField( 22 | model_name='logentry', 23 | name='action', 24 | field=models.CharField(db_index=True, default='old', max_length=100, verbose_name='action'), 25 | preserve_default=False, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /sberbank/serializers.py: -------------------------------------------------------------------------------- 1 | from sberbank.models import Payment, Status, Method 2 | from sberbank.util import system_name 3 | from rest_framework import serializers 4 | 5 | 6 | class PaymentSerializer(serializers.ModelSerializer): 7 | status = serializers.SerializerMethodField() 8 | pan = serializers.SerializerMethodField() 9 | system = serializers.SerializerMethodField() 10 | method = serializers.SerializerMethodField() 11 | 12 | @staticmethod 13 | def get_method(obj): 14 | return Method(obj.method).name 15 | 16 | @staticmethod 17 | def get_status(obj): 18 | return Status(obj.status).name 19 | 20 | @staticmethod 21 | def get_pan(obj): 22 | return obj.details.get('pan') 23 | 24 | def get_system(self, obj): 25 | return system_name(self.get_pan(obj)) 26 | 27 | class Meta: 28 | model = Payment 29 | fields = ['uid', 'amount', 'status', 'updated', 'pan', 'system', 'method'] 30 | -------------------------------------------------------------------------------- /sberbank/util.py: -------------------------------------------------------------------------------- 1 | def system_name(card_num): 2 | if not card_num: 3 | return None 4 | 5 | snum = str(card_num) 6 | if snum.startswith("2"): 7 | return "MIR" 8 | elif snum.startswith("30") or snum.startswith("36") or snum.startswith("38"): 9 | return "DINERSCLUB" 10 | elif snum.startswith("31") or snum.startswith("35"): 11 | return "JCB" 12 | elif snum.startswith("34") or snum.startswith("37"): 13 | return "AMEX" 14 | elif snum.startswith("4"): 15 | return "VISA" 16 | elif snum.startswith("50") or snum.startswith("56") or snum.startswith("57") or \ 17 | snum.startswith("58"): 18 | return "MAESTRO" 19 | elif snum.startswith("51") or snum.startswith("52") or snum.startswith("53") or \ 20 | snum.startswith("54") or snum.startswith("55"): 21 | return "MASTERCARD" 22 | elif snum.startswith("60"): 23 | return "DISCOVER" 24 | elif snum.startswith("62"): 25 | return "UNIONPAY" 26 | elif snum.startswith("63") or snum.startswith("67"): 27 | return "MAESTRO" 28 | elif snum.startswith("7"): 29 | return "UEK" 30 | return "UNKNOWN" 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Sergey Anufrienko 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /sberbank/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.utils.translation import ugettext as _ 3 | 4 | from sberbank.models import Payment, LogEntry 5 | 6 | 7 | @admin.register(Payment) 8 | class PaymentAdmin(admin.ModelAdmin): 9 | list_display = ( 10 | 'uid', 'bank_id', 'amount', 'status', 'created', 'updated', 11 | ) 12 | list_filter = ('status',) 13 | search_fields = ( 14 | 'uid', 'bank_id', 'amount' 15 | ) 16 | 17 | readonly_fields = ( 18 | 'created', 'updated', 'uid', 'bank_id', 'client_id', 'amount', 19 | 'status', 'method', 'details', 'error_code', 'error_message' 20 | ) 21 | 22 | fieldsets = ( 23 | ( 24 | None, 25 | { 26 | 'fields': [ 27 | ('uid', 'bank_id', 'client_id'), 28 | 'status', 'method', 29 | ('amount',), 30 | ] 31 | } 32 | ), 33 | ( 34 | _('More details'), 35 | { 36 | 'classes': ('collapse',), 37 | 'fields': ['details', 'error_code', 'error_message'] 38 | } 39 | ), 40 | ) 41 | 42 | 43 | @admin.register(LogEntry) 44 | class LogEntryAdmin(admin.ModelAdmin): 45 | list_display = ( 46 | 'uid', 'payment_id', 'bank_id', 'action', 'created', 47 | ) 48 | search_fields = ( 49 | 'uid', 'bank_id', 'payment_id', 'action' 50 | ) 51 | 52 | readonly_fields = ( 53 | 'created', 'uid', 'payment_id', 'bank_id', 'action', 54 | 'request_text', 'response_text', 'checksum') 55 | 56 | fieldsets = ( 57 | ( 58 | None, 59 | { 60 | 'fields': [ 61 | ('uid', 'payment_id', 'bank_id'), 62 | 'action', 63 | 'request_text', 64 | 'response_text', 65 | ] 66 | } 67 | ), 68 | ) 69 | -------------------------------------------------------------------------------- /sberbank/locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2018-11-18 20:15+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 20 | "%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" 21 | "%100>=11 && n%100<=14)? 2 : 3);\n" 22 | 23 | #: admin.py:32 24 | msgid "More details" 25 | msgstr "Больше деталей" 26 | 27 | #: apps.py:7 28 | msgid "sberbank" 29 | msgstr "сбербанк" 30 | 31 | #: models.py:48 32 | msgid "bank ID" 33 | msgstr "банковский ИД" 34 | 35 | #: models.py:49 36 | msgid "amount" 37 | msgstr "сумма" 38 | 39 | #: models.py:50 40 | msgid "error code" 41 | msgstr "код ошибки" 42 | 43 | #: models.py:51 44 | msgid "error message" 45 | msgstr "текст ошибки" 46 | 47 | #: models.py:52 48 | msgid "status" 49 | msgstr "состояние" 50 | 51 | #: models.py:54 52 | msgid "details" 53 | msgstr "детали" 54 | 55 | #: models.py:55 56 | msgid "client ID" 57 | msgstr "код клиента" 58 | 59 | #: models.py:56 60 | msgid "method" 61 | msgstr "метод" 62 | 63 | #: models.py:58 models.py:77 64 | msgid "created" 65 | msgstr "создан" 66 | 67 | #: models.py:59 68 | msgid "modified" 69 | msgstr "изменен" 70 | 71 | #: models.py:63 72 | msgid "payment" 73 | msgstr "платеж" 74 | 75 | #: models.py:64 76 | msgid "payments" 77 | msgstr "платежи" 78 | 79 | #: models.py:72 80 | msgid "payment ID" 81 | msgstr "ИД платежа" 82 | 83 | #: models.py:73 84 | msgid "bank payment ID" 85 | msgstr "банковский ИД платежа" 86 | 87 | #: models.py:74 88 | msgid "action" 89 | msgstr "действие" 90 | 91 | #: models.py:75 92 | msgid "request text" 93 | msgstr "текст запроса" 94 | 95 | #: models.py:76 96 | msgid "response text" 97 | msgstr "текст ответа" 98 | 99 | #: models.py:82 100 | msgid "log entry" 101 | msgstr "запись в журнале" 102 | 103 | #: models.py:83 104 | msgid "log entries" 105 | msgstr "записи в журнале" 106 | 107 | #: service.py:172 108 | msgid "card binding" 109 | msgstr "Привязка карты" 110 | -------------------------------------------------------------------------------- /sberbank/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.7 on 2018-07-28 13:51 2 | 3 | from django.db import migrations, models 4 | import jsonfield.encoder 5 | import jsonfield.fields 6 | import sberbank.models 7 | import uuid 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='LogEntry', 20 | fields=[ 21 | ('uid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 22 | ('payment_id', models.UUIDField(blank=True, db_index=True, null=True, verbose_name='payment ID')), 23 | ('bank_id', models.UUIDField(blank=True, db_index=True, null=True, verbose_name='bank payment ID')), 24 | ('request_type', models.CharField(choices=[(0, 'CREATE'), (1, 'CALLBACK'), (2, 'CHECK_STATUS')], db_index=True, max_length=1, verbose_name='request type')), 25 | ('response_text', models.TextField(blank=True, null=True, verbose_name='response text')), 26 | ('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='created')), 27 | ('checksum', models.CharField(blank=True, db_index=True, max_length=256, null=True)), 28 | ], 29 | options={ 30 | 'ordering': ['-created'], 31 | }, 32 | ), 33 | migrations.CreateModel( 34 | name='Payment', 35 | fields=[ 36 | ('uid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 37 | ('bank_id', models.UUIDField(blank=True, db_index=True, null=True, verbose_name='bank ID')), 38 | ('amount', models.DecimalField(decimal_places=2, max_digits=128, verbose_name='amount')), 39 | ('error_code', models.PositiveIntegerField(blank=True, null=True, verbose_name='error code')), 40 | ('error_message', models.TextField(blank=True, null=True, verbose_name='error message')), 41 | ('status', models.PositiveSmallIntegerField(choices=[(0, 'CREATED'), (1, 'PENDING'), (2, 'SUCCEEDED'), (3, 'FAILED')], db_index=True, default=sberbank.models.Status(0), verbose_name='status')), 42 | ('details', jsonfield.fields.JSONField(blank=True, dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={}, null=True, verbose_name='details')), 43 | ('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='created')), 44 | ('updated', models.DateTimeField(auto_now=True, db_index=True, verbose_name='modified')), 45 | ], 46 | options={ 47 | 'verbose_name': 'payment', 48 | 'ordering': ['-updated'], 49 | 'verbose_name_plural': 'payments', 50 | }, 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /sberbank/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from enum import IntEnum 3 | 4 | from jsonfield import JSONField 5 | from django.db import models 6 | from django.utils.translation import ugettext as _ 7 | 8 | 9 | class Choice(IntEnum): 10 | @classmethod 11 | def choices(cls): 12 | return [(x.value, x.name) for x in cls] 13 | 14 | 15 | class Status(Choice): 16 | CREATED = 0 17 | PENDING = 1 18 | SUCCEEDED = 2 19 | FAILED = 3 20 | REFUNDED = 4 21 | 22 | def __str__(self): 23 | return str(self.value) 24 | 25 | 26 | class Method(Choice): 27 | UNKNOWN = 0 28 | WEB = 1 29 | APPLE = 2 30 | GOOGLE = 3 31 | 32 | def __str__(self): 33 | return str(self.value) 34 | 35 | 36 | class Payment(models.Model): 37 | """ 38 | details JSON fields: 39 | username 40 | currency 41 | success_url 42 | fail_url 43 | session_timeout 44 | page_view 45 | redirect_url 46 | """ 47 | 48 | uid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 49 | bank_id = models.UUIDField(_("bank ID"), null=True, blank=True, db_index=True) 50 | amount = models.DecimalField(_("amount"), max_digits=128, decimal_places=2) 51 | error_code = models.PositiveIntegerField(_("error code"), null=True, blank=True) 52 | error_message = models.TextField(_("error message"), null=True, blank=True) 53 | status = models.PositiveSmallIntegerField(_("status"), choices=Status.choices(), 54 | default=Status.CREATED, db_index=True) 55 | details = JSONField(_("details"), blank=True, null=True) 56 | client_id = models.TextField(_("client ID"), null=True, blank=True) 57 | method = models.PositiveSmallIntegerField(_("method"), choices=Method.choices(), 58 | default=Method.UNKNOWN, db_index=True) 59 | created = models.DateTimeField(_("created"), auto_now_add=True, db_index=True) 60 | updated = models.DateTimeField(_("modified"), auto_now=True, db_index=True) 61 | 62 | class Meta: 63 | ordering = ['-updated'] 64 | verbose_name = _('payment') 65 | verbose_name_plural = _('payments') 66 | 67 | def __str__(self): 68 | return "%s: %s" % (Status(self.status).name, self.amount) 69 | 70 | 71 | class LogEntry(models.Model): 72 | uid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 73 | payment_id = models.UUIDField(_("payment ID"), null=True, blank=True, db_index=True) 74 | bank_id = models.UUIDField(_("bank payment ID"), null=True, blank=True, db_index=True) 75 | action = models.CharField(_("action"), max_length=100, db_index=True) 76 | request_text = models.TextField(_("request text"), null=True, blank=True) 77 | response_text = models.TextField(_("response text"), null=True, blank=True) 78 | created = models.DateTimeField(_("created"), auto_now_add=True, db_index=True) 79 | checksum = models.CharField(max_length=256, null=True, blank=True, db_index=True) 80 | 81 | class Meta: 82 | ordering = ['-created'] 83 | verbose_name = _('log entry') 84 | verbose_name_plural = _('log entries') 85 | -------------------------------------------------------------------------------- /sberbank/migrations/0005_auto_20180831_0901.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.7 on 2018-08-31 09:01 2 | 3 | from django.db import migrations, models 4 | import jsonfield.encoder 5 | import jsonfield.fields 6 | import sberbank.models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('sberbank', '0004_logentry_request_text'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterModelOptions( 17 | name='payment', 18 | options={'ordering': ['-updated'], 'verbose_name': 'платеж', 'verbose_name_plural': 'платежи'}, 19 | ), 20 | migrations.AlterField( 21 | model_name='logentry', 22 | name='action', 23 | field=models.CharField(db_index=True, max_length=100, verbose_name='действие'), 24 | ), 25 | migrations.AlterField( 26 | model_name='logentry', 27 | name='bank_id', 28 | field=models.UUIDField(blank=True, db_index=True, null=True, verbose_name='банковский ИД платежа'), 29 | ), 30 | migrations.AlterField( 31 | model_name='logentry', 32 | name='created', 33 | field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='создан'), 34 | ), 35 | migrations.AlterField( 36 | model_name='logentry', 37 | name='payment_id', 38 | field=models.UUIDField(blank=True, db_index=True, null=True, verbose_name='ИД платежа'), 39 | ), 40 | migrations.AlterField( 41 | model_name='logentry', 42 | name='request_text', 43 | field=models.TextField(blank=True, null=True, verbose_name='текст запроса'), 44 | ), 45 | migrations.AlterField( 46 | model_name='logentry', 47 | name='response_text', 48 | field=models.TextField(blank=True, null=True, verbose_name='текст ответа'), 49 | ), 50 | migrations.AlterField( 51 | model_name='payment', 52 | name='amount', 53 | field=models.DecimalField(decimal_places=2, max_digits=128, verbose_name='сумма'), 54 | ), 55 | migrations.AlterField( 56 | model_name='payment', 57 | name='bank_id', 58 | field=models.UUIDField(blank=True, db_index=True, null=True, verbose_name='БИК'), 59 | ), 60 | migrations.AlterField( 61 | model_name='payment', 62 | name='client_id', 63 | field=models.TextField(blank=True, null=True, verbose_name='код клиента'), 64 | ), 65 | migrations.AlterField( 66 | model_name='payment', 67 | name='created', 68 | field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='создан'), 69 | ), 70 | migrations.AlterField( 71 | model_name='payment', 72 | name='details', 73 | field=jsonfield.fields.JSONField(blank=True, dump_kwargs={'cls': jsonfield.encoder.JSONEncoder, 'separators': (',', ':')}, load_kwargs={}, null=True, verbose_name='детали'), 74 | ), 75 | migrations.AlterField( 76 | model_name='payment', 77 | name='error_code', 78 | field=models.PositiveIntegerField(blank=True, null=True, verbose_name='код ошибки'), 79 | ), 80 | migrations.AlterField( 81 | model_name='payment', 82 | name='error_message', 83 | field=models.TextField(blank=True, null=True, verbose_name='текст ошибки'), 84 | ), 85 | migrations.AlterField( 86 | model_name='payment', 87 | name='status', 88 | field=models.PositiveSmallIntegerField(choices=[(0, 'CREATED'), (1, 'PENDING'), (2, 'SUCCEEDED'), (3, 'FAILED')], db_index=True, default=sberbank.models.Status(0), verbose_name='состояние'), 89 | ), 90 | migrations.AlterField( 91 | model_name='payment', 92 | name='updated', 93 | field=models.DateTimeField(auto_now=True, db_index=True, verbose_name='обновлен'), 94 | ), 95 | ] 96 | -------------------------------------------------------------------------------- /sberbank/views.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha256 2 | import hmac 3 | import json 4 | 5 | from collections import OrderedDict 6 | 7 | from django.conf import settings 8 | from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseRedirect 9 | from django.utils.decorators import method_decorator 10 | from django.views.decorators.csrf import csrf_exempt 11 | 12 | from rest_framework.response import Response 13 | from rest_framework.views import APIView 14 | 15 | from sberbank.models import Payment, LogEntry, Status 16 | from sberbank.service import BankService 17 | from sberbank.serializers import PaymentSerializer 18 | 19 | 20 | class StatusView(APIView): 21 | @staticmethod 22 | def get(request, uid=None): 23 | try: 24 | payment = Payment.objects.get(uid=uid) 25 | except Payment.DoesNotExist: 26 | return HttpResponse(status=404) 27 | return Response({"status": Status(payment.status).name}) 28 | 29 | 30 | class BindingsView(APIView): 31 | @staticmethod 32 | def get(request, client_id=None): 33 | svc = BankService(settings.MERCHANT_KEY) 34 | return Response(svc.get_bindings(client_id)) 35 | 36 | class BindingView(APIView): 37 | authentication_classes = [] 38 | 39 | @staticmethod 40 | def delete(request, binding_id=None): 41 | svc = BankService(settings.MERCHANT_KEY) 42 | svc.deactivate_binding(binding_id) 43 | return HttpResponse(status=200) 44 | 45 | @method_decorator(csrf_exempt) 46 | def dispatch(self, *args, **kwargs): 47 | return super(BindingView, self).dispatch(*args, **kwargs) 48 | 49 | 50 | class GetHistoryView(APIView): 51 | @staticmethod 52 | def get(request, client_id=None, format=None): 53 | payments = Payment.objects.filter(client_id=client_id, status=Status.SUCCEEDED).order_by('-updated') 54 | serializer = PaymentSerializer(payments, many=True) 55 | return Response(serializer.data) 56 | 57 | 58 | def callback(request): 59 | data = OrderedDict(sorted(request.GET.items(), key=lambda x: x[0])) 60 | 61 | try: 62 | payment = Payment.objects.get(bank_id=data.get('mdOrder')) 63 | except Payment.DoesNotExist: 64 | return HttpResponse(status=200) 65 | 66 | merchant = settings.MERCHANTS.get(settings.MERCHANT_KEY) 67 | hash_key = merchant.get('hash_key') 68 | 69 | if hash_key: 70 | check_str = '' 71 | 72 | for key, value in data.items(): 73 | if key != 'checksum': 74 | check_str += "%s;%s;" % (key, value) 75 | 76 | checksum = hmac.new(hash_key.encode(), check_str.encode(), sha256) \ 77 | .hexdigest().upper() 78 | 79 | LogEntry.objects.create( 80 | action="callback", 81 | bank_id=payment.bank_id, 82 | payment_id=payment.uid, 83 | response_text=json.dumps(request.GET), 84 | checksum=checksum 85 | ) 86 | 87 | if checksum != data.get('checksum'): 88 | payment.status = Status.FAILED 89 | payment.save() 90 | return HttpResponseBadRequest('Checksum check failed') 91 | 92 | if int(data.get('status')) == 1: 93 | payment.status = Status.SUCCEEDED 94 | elif int(data.get('status')) == 0: 95 | payment.status = Status.FAILED 96 | 97 | payment.save() 98 | 99 | return HttpResponse(status=200) 100 | 101 | 102 | def redirect(request, kind=None): 103 | try: 104 | payment = Payment.objects.get(bank_id=request.GET.get('orderId')) 105 | except Payment.DoesNotExist: 106 | return HttpResponse(status=404) 107 | 108 | svc = BankService(settings.MERCHANT_KEY) 109 | svc.check_status(payment.uid) 110 | 111 | LogEntry.objects.create( 112 | action="redirect_%s" % kind, 113 | bank_id=payment.bank_id, 114 | payment_id=payment.uid, 115 | response_text=json.dumps(request.GET), 116 | ) 117 | 118 | svc.check_bind_refund(payment) 119 | 120 | merchant = settings.MERCHANTS.get(settings.MERCHANT_KEY) 121 | return HttpResponseRedirect("%s?payment=%s" % (merchant["app_%s_url" % kind], payment.uid)) 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI version fury.io](https://badge.fury.io/py/django-sberbank.svg)](https://pypi.python.org/pypi/django-sberbank/) 2 | [![PyPI license](https://img.shields.io/pypi/l/django-sberbank.svg)](https://pypi.python.org/pypi/django-sberbank/) 3 | 4 | # Оплата через платежный API Сбербанка в Django 5 | Это Django-приложение позволяет быстро приделать к сайту на Django прием оплаты с банковских карт с помощью платежного API Сбербанка. Приложение поддерживает: 6 | 7 | * Оплату через веб-формы (пользователь вводит данные карты на сервере Сбербанка) 8 | * Оплату с помощью Apple Pay и Google Pay 9 | * Привязку банковских карт и получение списка привязанных карт 10 | * Отслеживание истории транзакций и журналирование обмена с API Сбербанка в БД 11 | 12 | ## Установка 13 | 1. Добавить `sberbank` в список INSTALLED_APPS: 14 | ```python 15 | INSTALLED_APPS = [ 16 | ... 17 | 'sberbank', 18 | ... 19 | ] 20 | ``` 21 | 2. Добавить параметры мерчанта в `settings.py`: 22 | ```python 23 | MERCHANTS = { 24 | %merchant_id%: { 25 | 'username': '%merchant_username%', 26 | 'password': '%merchant_password%', 27 | 'success_url': 'http://ваш.домен/sberbank/payment/success', 28 | 'fail_url': 'http://ваш.домен/sberbank/payment/fail', 29 | 'app_success_url': 'http://ваш.домен/payment/success', 30 | 'app_fail_url': 'http://ваш.домен/payment/fail', 31 | } 32 | } 33 | ``` 34 | 3. Добавить URL-ы приложения в ваш `urls.py`: 35 | ```python 36 | urlpatterns = [ 37 | ... 38 | url('/sberbank', include('sberbank.urls')) 39 | ] 40 | 41 | ``` 42 | 4. Запустить `python manage.py migrate` чтобы создать модели. 43 | 44 | ## Установка окружения 45 | 46 | Переменная окружения: `ENVIRONMENT` 47 | 48 | Возможные значения: 49 | * `development` - https://securepayments.sberbank.ru/payment 50 | * `production` - https://3dsec.sberbank.ru/payment 51 | 52 | По-умолчанию: `development` 53 | 54 | ## Параметры словаря `MERCHANTS` 55 | * `success_url` - на данный URL Сбербанк будет перенаправлять браузер после успешного платежа 56 | * `fail_url` - на данный URL Сбербанк будет перенаправлять браузер после неуспешного платежа 57 | * `app_success_url` - это URL, с помощью которого ваше приложение может среагировать на успешный платеж после того, как отработает коллбэк `success_url`. 58 | * `app_fail_url` - это URL, с помощью которого ваше приложение может среагировать на неуспешный платеж после того, как отработает коллбэк `fail_url`. 59 | 60 | ## Использование 61 | ### Платеж с помощью веб-формы 62 | 63 | ```python 64 | from sberbank.service import BankService 65 | from sberbank.models import Payment, Status 66 | 67 | ... 68 | try: 69 | # Сумма в рублях 70 | amount = 10.0 71 | 72 | # Уникальный ID пользователя, используется для привязки карт 73 | # Если None, пользователь не сможет выбрать ранее привязанную карту 74 | # или привязать карту в процессе оплаты 75 | client_id = request.data.get("client_id") 76 | 77 | svc = BankService(%merchant_id%) 78 | 79 | # url - адрес, на который следует перенаправить пользователя для оплаты 80 | # payment - объект Payment из БД, содержит информацию о платеже 81 | # description - назначение платежа в веб-форме банка 82 | # params - произвольные параметры, которые можно привязать к платежу 83 | payment, url = svc.pay(amount, params={'foo': 'bar'}, client_id=client_id, 84 | description="Оплата заказа №1234") 85 | return HttpResponseRedirect(url) 86 | except Exception as exc: 87 | # Что-то пошло не так 88 | raise 89 | ``` 90 | ### Привязка карты со списанием и возвратом 1 рубля 91 | 92 | ```python 93 | from sberbank.service import BankService 94 | from sberbank.models import Payment, Status 95 | 96 | ... 97 | try: 98 | # Уникальный ID пользователя, используется для привязки карт 99 | # параметр необходимо передавать при использовании функции привязки карт 100 | # через списание и возврат 101 | client_id = request.data.get("client_id") 102 | if client_id is None: 103 | return HttpResponseBadRequest() 104 | 105 | svc = BankService(%merchant_id%) 106 | 107 | # url - адрес, на который следует перенаправить пользователя для оплаты 108 | # payment - объект Payment из БД, содержит информацию о платеже 109 | payment, url = svc.bind_refund(client_id=client_id) 110 | return HttpResponseRedirect(url) 111 | except Exception as exc: 112 | # Что-то пошло не так 113 | raise 114 | ``` 115 | ### Оплата с помощью Apple/Google Pay 116 | 117 | ```python 118 | from sberbank.service import BankService 119 | from sberbank.models import Payment, Status 120 | 121 | ... 122 | try: 123 | # Уникальный ID пользователя, используется для привязки карт 124 | # параметр необходимо передавать при использовании функции привязки карт 125 | # через списание и возврат 126 | client_id = request.data.get("client_id") 127 | 128 | # Сумма платежа в рублях 129 | amount = 10.0 130 | 131 | # Токен, переданный приложением для Apple/Android 132 | # библиотека сама определяет тип платежа по формату 133 | # переданного токена и вызывает соответствующее API Сбербанка 134 | token = request.data.get("token") 135 | 136 | # IP адрес клиента 137 | ip = request.META.get('REMOTE_ADDR', '127.0.0.1') 138 | 139 | svc = BankService(%merchant_id%) 140 | 141 | payment, response = svc.mobile_pay(amount, token, ip, client_id=client_id, 142 | params={'foo': 'bar'}, description="Оплата заказа №1234") 143 | 144 | if response['success'] != True: 145 | return Response({"status": "error"}) 146 | if payment.status == Status.SUCCEEDED: 147 | # Платеж успешен 148 | json_response = {"status": "success"} 149 | 150 | # Платежи с некоторых карт требуют особой обработки на клиенте 151 | # При наличии в ответе acsUrl клиенту нужно перенаправить пользователя 152 | # на адрес redirect_url, POST-запросом передав параметры в виде x-www-form-urlencoded 153 | if response['data'].get('acsUrl'): 154 | json_response.update({"redirect_url": response['data'].get('acsUrl', '')}) 155 | json_response.update({"params": { 156 | 'paReq': response['data'].get('paReq', ''), 157 | 'termUrl': response['data'].get('termUrl', ''), 158 | 'orderId': response['data'].get('orderId', '')}}) 159 | return Response(json_response) 160 | except Exception as exc: 161 | # Что-то пошло не так 162 | raise 163 | ``` 164 | 165 | ### Периодическая проверка платежей по которым не известно состояние 166 | 167 | ```python 168 | from datetime import timedelta 169 | from celery.task import periodic_task 170 | from sberbank.tasks import check_payments 171 | 172 | @periodic_task(run_every=timedelta(minutes=20)) 173 | def task_check_payments(): 174 | check_payments() 175 | ``` 176 | -------------------------------------------------------------------------------- /sberbank/service.py: -------------------------------------------------------------------------------- 1 | import json 2 | import base64 3 | from decimal import Decimal, DecimalException 4 | 5 | import requests 6 | from django.conf import settings 7 | from django.utils.translation import ugettext as _ 8 | 9 | from sberbank.exceptions import NetworkException, ProcessingException, \ 10 | PaymentNotFoundException 11 | from sberbank.models import Payment, LogEntry, Status, Method 12 | from sberbank.util import system_name 13 | 14 | 15 | class BankService(object): 16 | __default_session_timeout = 1200 17 | __default_currency_code = 643 18 | __default_gateway_address = 'https://3dsec.sberbank.ru/payment' 19 | 20 | def __init__(self, merchant_id): 21 | if getattr(settings, 'ENVIRONMENT', 'development') == 'production': 22 | self.__default_gateway_address = \ 23 | 'https://securepayments.sberbank.ru/payment' 24 | self._get_credentials(merchant_id) 25 | self.merchant_id = merchant_id 26 | 27 | def _get_credentials(self, merchant_id): 28 | settings_merchant_key = "MERCHANTS" 29 | 30 | merchants = getattr(settings, settings_merchant_key, None) 31 | if merchants is None: 32 | raise KeyError( 33 | "Key %s not found in settings.py" % settings_merchant_key) 34 | 35 | self.merchant = merchants.get(merchant_id, None) 36 | if self.merchant is None: 37 | raise KeyError( 38 | "Merchant key %s not found in %s" % ( 39 | merchant_id, settings_merchant_key)) 40 | 41 | for field_name in ["username", "password"]: 42 | if self.merchant.get(field_name, None) is None: 43 | raise KeyError( 44 | "Field '%s' not found in %s->%s" % ( 45 | field_name, settings_merchant_key, merchant_id)) 46 | 47 | def mobile_pay(self, amount, token, ip, **kwargs): 48 | currency = self.merchant.get('currency', self.__default_currency_code) 49 | client_id = kwargs.get('client_id') 50 | details = kwargs.get('details', {}) 51 | fail_url = kwargs.get('fail_url', self.merchant.get('fail_url')) 52 | success_url = kwargs.get('success_url', self.merchant.get('success_url')) 53 | method = "applepay/payment" 54 | db_method = Method.APPLE 55 | 56 | try: 57 | decoded_token = json.loads(base64.b64decode(token).decode()) 58 | if "signedMessage" in decoded_token: 59 | method = "google/payment" 60 | db_method = Method.GOOGLE 61 | 62 | except Exception: 63 | raise TypeError("Failed to decode payment token") 64 | 65 | try: 66 | amount = Decimal(str(amount)) 67 | except (ValueError, DecimalException): 68 | raise TypeError( 69 | "Wrong amount type, passed {} ({}) instead of decimal".format(amount, type(amount))) 70 | 71 | payment = Payment(amount=amount, client_id=client_id, method=db_method, details={ 72 | 'username': self.merchant.get("username"), 73 | 'currency': currency 74 | }) 75 | 76 | if kwargs.get('params'): 77 | payment.details.update(kwargs.get('params')) 78 | 79 | payment.details.update(details) 80 | payment.status = Status.PENDING 81 | payment.save() 82 | 83 | data = { 84 | 'merchant': self.merchant_id, 85 | 'orderNumber': payment.uid.hex, 86 | 'paymentToken': token, 87 | 'ip': ip 88 | } 89 | if method == "google/payment": 90 | data.update({ 91 | 'amount': int(amount * 100), 92 | 'returnUrl': success_url, 93 | 'failUrl': fail_url 94 | }) 95 | if kwargs.get('params'): 96 | data.update({'additionalParameters': kwargs.get('params')}) 97 | if kwargs.get('description'): 98 | data.update({'description': kwargs.get('description')}) 99 | 100 | response = self.execute_request(data, method, payment) 101 | 102 | if response.get('success'): 103 | payment.bank_id = response.get('data').get('orderId') 104 | if 'orderStatus' in response: 105 | payment.details.update({"pan": response['orderStatus']['cardAuthInfo']['pan']}) 106 | else: 107 | payment.status = Status.FAILED 108 | 109 | payment.save() 110 | if payment.status != Status.FAILED: 111 | payment = self.check_status(payment.uid) 112 | return payment, response 113 | 114 | def pay(self, amount, preauth=False, **kwargs): 115 | session_timeout = self.merchant.get('session_timeout', self.__default_session_timeout) 116 | currency = self.merchant.get('currency', self.__default_currency_code) 117 | fail_url = kwargs.get('fail_url', self.merchant.get('fail_url')) 118 | success_url = kwargs.get('success_url', self.merchant.get('success_url')) 119 | client_id = kwargs.get('client_id') 120 | page_view = kwargs.get('page_view', 'DESKTOP') 121 | details = kwargs.get('details', {}) 122 | description = kwargs.get('description') 123 | binding_id = kwargs.get('binding_id', None) 124 | method = 'rest/register' if not preauth else 'rest/registerPreAuth' 125 | 126 | if success_url is None: 127 | raise ValueError("success_url is not set") 128 | 129 | try: 130 | amount = Decimal(str(amount)) 131 | except (ValueError, DecimalException): 132 | raise TypeError( 133 | "Wrong amount type, passed {} ({}) instead of decimal".format(amount, type(amount))) 134 | 135 | payment = Payment(amount=amount, client_id=client_id, method=Method.WEB, details={ 136 | 'username': self.merchant.get("username"), 137 | 'currency': currency, 138 | 'success_url': success_url, 139 | 'fail_url': fail_url, 140 | 'session_timeout': session_timeout, 141 | 'client_id': client_id 142 | }) 143 | 144 | payment.details.update(details) 145 | payment.save() 146 | 147 | data = { 148 | 'orderNumber': payment.uid.hex, 149 | 'amount': int(amount * 100), 150 | 'returnUrl': success_url, 151 | 'failUrl': fail_url, 152 | 'sessionTimeoutSecs': session_timeout, 153 | 'pageView': page_view, 154 | } 155 | if kwargs.get('params'): 156 | data.update({'jsonParams': json.dumps(kwargs.get('params'))}) 157 | if kwargs.get('client_id'): 158 | data.update({'clientId': client_id}) 159 | if kwargs.get('description'): 160 | data.update({'description': description}) 161 | if kwargs.get('binding_id'): 162 | data.update({'bindingId': binding_id}) 163 | 164 | response = self.execute_request(data, method, payment) 165 | 166 | payment.bank_id = response.get('orderId') 167 | payment.status = Status.PENDING 168 | payment.details.update({'redirect_url': response.get('formUrl')}) 169 | if kwargs.get('params'): 170 | payment.details.update(kwargs.get('params')) 171 | payment.save() 172 | 173 | return payment, payment.details.get("redirect_url") 174 | 175 | def bind_refund(self, client_id): 176 | return self.pay(1.0, client_id=client_id, 177 | preauth=True, page_view="bind", details={"bind_refund": True}, 178 | description=_("card binding")) 179 | 180 | def check_bind_refund(self, payment): 181 | if payment.details.get('bind_refund', False) and \ 182 | payment.status in (Status.PENDING, Status.SUCCEEDED): 183 | self.reverse(payment) 184 | 185 | def reverse(self, payment): 186 | return self.execute_request({'orderId': str(payment.bank_id)}, "rest/reverse", payment) 187 | 188 | def check_status(self, payment_uid): 189 | try: 190 | payment = Payment.objects.get(pk=payment_uid) 191 | except Payment.DoesNotExist: 192 | raise PaymentNotFoundException() 193 | 194 | data = {'orderId': str(payment.bank_id)} 195 | response = self.execute_request(data, "rest/getOrderStatusExtended", payment) 196 | 197 | if response.get('orderStatus') == 2: 198 | payment.status = Status.SUCCEEDED 199 | payment.details.update({"pan": response['cardAuthInfo']['pan']}) 200 | elif response.get('orderStatus') in [3, 6]: 201 | payment.status = Status.FAILED 202 | elif response.get('orderStatus') == 4: 203 | payment.status = Status.REFUNDED 204 | 205 | payment.save(update_fields=['status', 'details']) 206 | return payment 207 | 208 | def get_bindings(self, client_id): 209 | def convert(entry): 210 | return { 211 | 'binding': entry['bindingId'], 212 | 'masked_pan': entry['maskedPan'], 213 | 'expiry_date': entry['expiryDate'], 214 | 'system': system_name(entry['maskedPan']) 215 | } 216 | 217 | try: 218 | response = self.execute_request({"clientId": client_id}, "rest/getBindings") 219 | return list(map(convert, response.get('bindings'))) 220 | except ProcessingException as exc: 221 | if exc.error_code == 2: 222 | return [] 223 | 224 | def deactivate_binding(self, binding_id): 225 | self.execute_request({'bindingId': binding_id}, "rest/unBindCard") 226 | 227 | def execute_request(self, data, method, payment=None): 228 | rest = method.startswith("rest/") 229 | 230 | headers = { 231 | "Accept": "application/json" 232 | } 233 | 234 | if rest: 235 | data.update({ 236 | "userName": self.merchant.get('username'), 237 | 'password': self.merchant.get('password') 238 | }) 239 | else: 240 | headers.update({"Content-Type": "application/json"}) 241 | data = json.dumps(data) 242 | 243 | try: 244 | response = requests.post( 245 | '{}/{}.do'.format(self.__default_gateway_address, method), data=data, headers=headers) 246 | except (requests.ConnectTimeout, 247 | requests.ConnectionError, 248 | requests.HTTPError): 249 | if payment: 250 | payment.status = Status.FAILED 251 | payment.save() 252 | raise NetworkException(payment.uid if payment else None) 253 | 254 | if rest: 255 | data.update({'password': '****'}) 256 | 257 | LogEntry.objects.create( 258 | action=method, 259 | bank_id=payment.bank_id if payment else None, 260 | payment_id=payment.uid if payment else None, 261 | response_text=response.text, request_text=json.dumps(data) if rest else data) 262 | 263 | if response.status_code != 200: 264 | if payment: 265 | payment.status = Status.FAILED 266 | payment.save() 267 | raise ProcessingException(payment.uid if payment else None, response.text, 268 | response.status_code) 269 | try: 270 | response = response.json() 271 | except (ValueError, UnicodeDecodeError): 272 | if payment: 273 | payment.status = Status.FAILED 274 | payment.save() 275 | raise ProcessingException(payment.uid if payment.uid else None) 276 | 277 | if int(response.get('errorCode', 0)) != 0: 278 | if payment: 279 | payment.error_code = response.get('errorCode') 280 | payment.error_message = response.get('errorMessage') 281 | payment.status = Status.FAILED 282 | payment.save() 283 | raise ProcessingException(payment.uid if payment else None, response.get('errorMessage'), 284 | response.get('errorCode')) 285 | 286 | return response 287 | --------------------------------------------------------------------------------