From 808ccfee752cd52132a07c9b3c5d86b9aa07f5ca Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Wed, 20 Nov 2019 18:35:15 +0100 Subject: [PATCH] Bank import: useful matching of negative transactions Negative transactions are never matched automatically, which does not change. However, when matching them manually, a negative payment was created, which does not make much sense. Now, if a negative payment is manually matched, the system checks whether: a) There is a manual refund in pending state. In this case, the refund will be marked as done. b) There is a bank transfer payment of the same amount, in which case this is handled like a credit card chargeback (i.e. notification on the dashboard, ...) c) Otherwise, an error will be returned asking the user to create a refund object manually. --- src/pretix/plugins/banktransfer/views.py | 42 +++++++++- .../plugins/banktransfer/test_actions.py | 76 ++++++++++++++++++- 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/src/pretix/plugins/banktransfer/views.py b/src/pretix/plugins/banktransfer/views.py index 6f042bf324..a0ed667d2f 100644 --- a/src/pretix/plugins/banktransfer/views.py +++ b/src/pretix/plugins/banktransfer/views.py @@ -2,6 +2,7 @@ import csv import json import logging from datetime import timedelta +from decimal import Decimal from django.contrib import messages from django.db.models import Count, Q @@ -14,7 +15,7 @@ from django.utils.timezone import now from django.utils.translation import ugettext as _ from django.views.generic import DetailView, ListView, View -from pretix.base.models import Order, OrderPayment, Quota +from pretix.base.models import Order, OrderPayment, OrderRefund, Quota from pretix.base.services.mail import SendMailException from pretix.base.settings import SettingsSandbox from pretix.base.templatetags.money import money_filter @@ -44,6 +45,45 @@ class ActionView(View): return self._accept_ignore_amount(trans) def _accept_ignore_amount(self, trans): + if trans.amount < Decimal('0.00'): + ref = trans.order.refunds.filter( + amount=trans.amount * -1, + provider='manual', + state__in=(OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_CREATED) + ).first() + p = trans.order.payments.filter( + amount=trans.amount * -1, + provider='banktransfer', + state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED) + ).first() + if ref: + ref.done(user=self.request.user) + trans.state = BankTransaction.STATE_VALID + trans.save() + return JsonResponse({ + 'status': 'ok', + }) + elif p: + p.create_external_refund( + amount=trans.amount * -1, + info=json.dumps({ + 'reference': trans.reference, + 'date': trans.date, + 'payer': trans.payer, + 'trans_id': trans.pk + }) + ) + trans.state = BankTransaction.STATE_VALID + trans.save() + return JsonResponse({ + 'status': 'ok', + }) + else: + return JsonResponse({ + 'status': 'error', + 'message': _('Negative amount but refund can\'t be logged, please create manual refund first.') + }) + if trans.order.status == Order.STATUS_PAID: return JsonResponse({ 'status': 'error', diff --git a/src/tests/plugins/banktransfer/test_actions.py b/src/tests/plugins/banktransfer/test_actions.py index f991e9c5f6..d195a73757 100644 --- a/src/tests/plugins/banktransfer/test_actions.py +++ b/src/tests/plugins/banktransfer/test_actions.py @@ -3,9 +3,11 @@ from datetime import timedelta import pytest from django.utils.timezone import now +from django_scopes import scopes_disabled from pretix.base.models import ( - Event, Item, Order, OrderPosition, Organizer, Quota, Team, User, + Event, Item, Order, OrderPayment, OrderPosition, OrderRefund, Organizer, + Quota, Team, User, ) from pretix.plugins.banktransfer.models import BankImportJob, BankTransaction @@ -256,3 +258,75 @@ def test_assign_order_organizer_no_permission_for_event(env, client): 'action_{}'.format(trans.pk): 'assign:{}-{}'.format(env[0].slug.upper(), env[2].code), }).content.decode('utf-8')) assert r['status'] == 'error' + + +@pytest.mark.django_db +def test_retry_refund(env, client): + job = BankImportJob.objects.create(event=env[0]) + trans = BankTransaction.objects.create(event=env[0], import_job=job, payer='Foo', + state=BankTransaction.STATE_ERROR, + amount=-23, date='unknown', order=env[3]) + client.login(email='dummy@dummy.dummy', password='dummy') + env[3].status = Order.STATUS_PAID + env[3].save() + r = json.loads(client.post('/control/event/{}/{}/banktransfer/action/'.format(env[0].organizer.slug, env[0].slug), { + 'action_{}'.format(trans.pk): 'retry', + }).content.decode('utf-8')) + assert r['status'] == 'error' + trans.refresh_from_db() + assert trans.state == BankTransaction.STATE_ERROR + + +@pytest.mark.django_db +def test_retry_refund_external(env, client): + job = BankImportJob.objects.create(event=env[0]) + trans = BankTransaction.objects.create(event=env[0], import_job=job, payer='Foo', + state=BankTransaction.STATE_ERROR, + amount=-23, date='unknown', order=env[3]) + with scopes_disabled(): + p = env[3].payments.create(amount=23, provider='banktransfer', state=OrderPayment.PAYMENT_STATE_CONFIRMED) + client.login(email='dummy@dummy.dummy', password='dummy') + env[3].status = Order.STATUS_PAID + env[3].save() + r = json.loads(client.post('/control/event/{}/{}/banktransfer/action/'.format(env[0].organizer.slug, env[0].slug), { + 'action_{}'.format(trans.pk): 'retry', + }).content.decode('utf-8')) + assert r['status'] == 'ok' + trans.refresh_from_db() + assert trans.state == BankTransaction.STATE_VALID + env[3].refresh_from_db() + assert env[3].status == Order.STATUS_PAID + with scopes_disabled(): + r = env[3].refunds.first() + assert r + assert r.provider == "banktransfer" + assert r.amount == 23 + assert r.payment == p + assert r.state == OrderRefund.REFUND_STATE_EXTERNAL + + +@pytest.mark.django_db +def test_retry_refund_complete(env, client): + job = BankImportJob.objects.create(event=env[0]) + trans = BankTransaction.objects.create(event=env[0], import_job=job, payer='Foo', + state=BankTransaction.STATE_ERROR, + amount=-23, date='unknown', order=env[3]) + with scopes_disabled(): + env[3].payments.create(amount=23, provider='banktransfer', state=OrderPayment.PAYMENT_STATE_CONFIRMED) + ref = env[3].refunds.create(amount=23, provider='manual', state=OrderRefund.REFUND_STATE_CREATED) + client.login(email='dummy@dummy.dummy', password='dummy') + env[3].status = Order.STATUS_CANCELED + env[3].save() + r = json.loads(client.post('/control/event/{}/{}/banktransfer/action/'.format(env[0].organizer.slug, env[0].slug), { + 'action_{}'.format(trans.pk): 'retry', + }).content.decode('utf-8')) + assert r['status'] == 'ok' + trans.refresh_from_db() + assert trans.state == BankTransaction.STATE_VALID + env[3].refresh_from_db() + assert env[3].status == Order.STATUS_CANCELED + ref.refresh_from_db() + assert ref.provider == "manual" + assert ref.amount == 23 + assert ref.payment is None + assert ref.state == OrderRefund.REFUND_STATE_DONE