lingo: add validation/cancellation endpoints (#12766)

This commit is contained in:
Frédéric Péters 2016-08-21 12:53:03 +02:00
parent ab4a5cdb10
commit 1b6ac4c4b2
6 changed files with 198 additions and 4 deletions

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import jsonfield.fields
class Migration(migrations.Migration):
dependencies = [
('lingo', '0020_auto_20160606_1803'),
]
operations = [
migrations.CreateModel(
name='TransactionOperation',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('kind', models.CharField(max_length=65, choices=[(b'validation', 'Validation'), (b'cancellation', 'Cancellation')])),
('amount', models.DecimalField(max_digits=8, decimal_places=2)),
('creation_date', models.DateTimeField(auto_now_add=True)),
('bank_result', jsonfield.fields.JSONField(default=dict, blank=True)),
('transaction', models.ForeignKey(to='lingo.Transaction')),
],
),
]

View File

@ -222,8 +222,9 @@ class BasketItem(models.Model):
message = {'result': 'ok'}
if status == 'paid':
transaction = self.transaction_set.filter(status=eopayment.PAID)[0]
message['transaction_id'] = transaction.id
message['order_id'] = transaction.order_id
message['transaction_id'] = transaction.bank_transaction_id
message['bank_transaction_id'] = transaction.bank_transaction_id
message['bank_data'] = transaction.bank_data
headers = {'content-type': 'application/json'}
r = requests.post(url, data=json.dumps(message), headers=headers, timeout=3)
@ -301,6 +302,19 @@ class Transaction(models.Model):
EXPIRED: _('Expired')
}.get(self.status) or _('Unknown')
class TransactionOperation(models.Model):
OPERATIONS = [
('validation', _('Validation')),
('cancellation', _('Cancellation')),
]
transaction = models.ForeignKey(Transaction)
kind = models.CharField(max_length=65, choices=OPERATIONS)
amount = models.DecimalField(decimal_places=2, max_digits=8)
creation_date = models.DateTimeField(auto_now_add=True)
bank_result = JSONField(blank=True)
@register_cell_class
class LingoBasketCell(CellBase):

View File

@ -20,7 +20,8 @@ from combo.urls_utils import decorated_includes, manager_required
from .views import (RegiesApiView, AddBasketItemApiView, PayView, CallbackView,
ReturnView, ItemDownloadView, ItemView, CancelItemView,
RemoveBasketItemApiView)
RemoveBasketItemApiView, ValidateTransactionApiView,
CancelTransactionApiView)
from .manager_views import (RegieListView, RegieCreateView, RegieUpdateView,
RegieDeleteView, TransactionListView, ManagerHomeView)
@ -41,6 +42,10 @@ urlpatterns = patterns('',
name='api-add-basket-item'),
url('^api/lingo/remove-basket-item$', RemoveBasketItemApiView.as_view(),
name='api-remove-basket-item'),
url('^api/lingo/validate-transaction$', ValidateTransactionApiView.as_view(),
name='api-validate-transaction'),
url('^api/lingo/cancel-transaction$', CancelTransactionApiView.as_view(),
name='api-cancel-transaction'),
url(r'^lingo/pay$', PayView.as_view(), name='lingo-pay'),
url(r'^lingo/cancel/(?P<pk>\w+)/$', CancelItemView.as_view(), name='lingo-cancel-item'),
url(r'^lingo/callback/(?P<regie_pk>\w+)/$', CallbackView.as_view(), name='lingo-callback'),

View File

@ -44,7 +44,8 @@ try:
except ImportError:
UserSAMLIdentifier = None
from .models import Regie, BasketItem, Transaction, LingoBasketCell
from .models import (Regie, BasketItem, Transaction, TransactionOperation,
LingoBasketCell)
def get_eopayment_object(request, regie):
options = regie.service_options
@ -186,6 +187,90 @@ class RemoveBasketItemApiView(View):
return response
class ValidateTransactionApiView(View):
http_method_names = ['post', 'options']
@csrf_exempt
def dispatch(self, *args, **kwargs):
return super(ValidateTransactionApiView, self).dispatch(*args, **kwargs)
def post(self, request, *args, **kwargs):
key = getattr(settings, 'LINGO_API_SIGN_KEY', '12345')
if not check_query(request.META['QUERY_STRING'], key):
return HttpResponseForbidden()
logger = logging.getLogger(__name__)
try:
transaction = Transaction.objects.get(id=request.GET['transaction_id'])
except Transaction.DoesNotExist:
logger.warning(u'received validate request for unknown transaction %s',
request.GET['transaction_id'])
raise Http404
payment = get_eopayment_object(request, transaction.regie)
amount = request.GET['amount']
logger.info(u'validating amount %s for transaction %s', amount, smart_text(transaction.id))
try:
result = payment.backend.validate(Decimal(amount), transaction.bank_data)
except eopayment.ResponseError as e:
logger.error(u'failed in validation operation: %s', e)
response = HttpResponse(content_type='application/json')
response.write(json.dumps({'err': 1, 'e': unicode(e)}))
return response
logger.info(u'bank validation result: %r', result)
operation = TransactionOperation(transaction=transaction,
kind='validation', amount=Decimal(amount), bank_result=result)
operation.save()
response = HttpResponse(content_type='application/json')
response.write(json.dumps({'err': 0, 'extra': result}))
return response
class CancelTransactionApiView(View):
http_method_names = ['post', 'options']
@csrf_exempt
def dispatch(self, *args, **kwargs):
return super(CancelTransactionApiView, self).dispatch(*args, **kwargs)
def post(self, request, *args, **kwargs):
key = getattr(settings, 'LINGO_API_SIGN_KEY', '12345')
if not check_query(request.META['QUERY_STRING'], key):
return HttpResponseForbidden()
logger = logging.getLogger(__name__)
try:
transaction = Transaction.objects.get(id=request.GET['transaction_id'])
except Transaction.DoesNotExist:
logger.warning(u'received validate request for unknown transaction %s',
request.GET['transaction_id'])
raise Http404
payment = get_eopayment_object(request, transaction.regie)
amount = request.GET['amount']
logger.info(u'cancelling amount %s for transaction %s', amount, smart_text(transaction.id))
try:
result = payment.backend.cancel(Decimal(amount), transaction.bank_data)
except eopayment.ResponseError as e:
logger.error(u'failed in cancel operation: %s', e)
response = HttpResponse(content_type='application/json')
response.write(json.dumps({'err': 1, 'e': unicode(e)}))
return response
logger.info(u'bank cancellation result: %r', result)
operation = TransactionOperation(transaction=transaction,
kind='cancellation', amount=Decimal(amount), bank_result=result)
operation.save()
response = HttpResponse(content_type='application/json')
response.write(json.dumps({'err': 0, 'extra': result}))
return response
class PayView(View):
@atomic
def post(self, request, *args, **kwargs):

View File

@ -184,6 +184,8 @@ def ellipsize(text, length=50):
def check_query(query, key, known_nonce=None, timedelta=30):
parsed = urlparse.parse_qs(query)
if not 'signature' in parsed:
return False
signature = base64.b64decode(parsed['signature'][0])
algo = parsed['algo'][0]
timestamp = parsed['timestamp'][0]

View File

@ -5,6 +5,7 @@ import urlparse
import urllib
from decimal import Decimal
import json
import mock
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
@ -15,7 +16,8 @@ from webtest import TestApp
from django.test import Client
from combo.apps.lingo.models import Regie, BasketItem, Transaction, RemoteItem, EXPIRED
from combo.apps.lingo.models import (Regie, BasketItem, Transaction,
TransactionOperation, RemoteItem, EXPIRED)
from combo.apps.lingo.management.commands.update_transactions import Command as UpdateTransactionsCommand
from combo.utils import sign_url
@ -214,3 +216,63 @@ def test_transaction_expiration():
assert Transaction.objects.get(id=t1.id).status == EXPIRED
assert Transaction.objects.get(id=t2.id).status == 0
def test_transaction_validate(regie, user):
t1 = Transaction(regie=regie, bank_data={'bank': 'data'}, amount=12,
status=eopayment.PAID)
t1.save()
url = reverse('api-validate-transaction') + '?amount=10&transaction_id=0'
resp = client.post(url, content_type='application/json')
assert resp.status_code == 403
url = reverse('api-validate-transaction') + '?amount=10&transaction_id=0'
url = sign_url(url, settings.LINGO_API_SIGN_KEY)
resp = client.post(url, content_type='application/json')
assert resp.status_code == 404
url = reverse('api-validate-transaction') + '?amount=10&transaction_id=%s' % t1.id
url = sign_url(url, settings.LINGO_API_SIGN_KEY)
resp = client.post(url, content_type='application/json')
assert json.loads(resp.content)['err'] == 0
operations = TransactionOperation.objects.filter(transaction=t1)
assert len(operations) == 1
assert operations[0].amount == 10
with mock.patch.object(eopayment.dummy.Payment, 'validate', autospec=True) as mock_validate:
mock_validate.side_effect = eopayment.ResponseError
url = reverse('api-validate-transaction') + '?amount=10&transaction_id=%s' % t1.id
url = sign_url(url, settings.LINGO_API_SIGN_KEY)
resp = client.post(url, content_type='application/json')
assert json.loads(resp.content)['err'] == 1
assert TransactionOperation.objects.filter(transaction=t1).count() == 1
def test_transaction_cancel(regie, user):
t1 = Transaction(regie=regie, bank_data={'bank': 'data'}, amount=12,
status=eopayment.PAID)
t1.save()
url = reverse('api-cancel-transaction') + '?amount=10&transaction_id=0'
resp = client.post(url, content_type='application/json')
assert resp.status_code == 403
url = reverse('api-cancel-transaction') + '?amount=10&transaction_id=0'
url = sign_url(url, settings.LINGO_API_SIGN_KEY)
resp = client.post(url, content_type='application/json')
assert resp.status_code == 404
url = reverse('api-cancel-transaction') + '?amount=10&transaction_id=%s' % t1.id
url = sign_url(url, settings.LINGO_API_SIGN_KEY)
resp = client.post(url, content_type='application/json')
assert json.loads(resp.content)['err'] == 0
operations = TransactionOperation.objects.filter(transaction=t1)
assert len(operations) == 1
assert operations[0].amount == 10
with mock.patch.object(eopayment.dummy.Payment, 'cancel', autospec=True) as mock_cancel:
mock_cancel.side_effect = eopayment.ResponseError
url = reverse('api-cancel-transaction') + '?amount=10&transaction_id=%s' % t1.id
url = sign_url(url, settings.LINGO_API_SIGN_KEY)
resp = client.post(url, content_type='application/json')
assert json.loads(resp.content)['err'] == 1
assert TransactionOperation.objects.filter(transaction=t1).count() == 1