diff options
author | Magnus Hagander | 2016-01-20 11:43:39 +0000 |
---|---|---|
committer | Magnus Hagander | 2016-01-20 18:56:38 +0000 |
commit | ee5cd99ec2b9aed13c30592199d9661e17189dd3 (patch) | |
tree | d3c97f5862f7f277cf7934ad4fda6687a92accaa /postgresqleu/adyen/util.py | |
parent | d062ccfe9841a27ecab4b0e838cdee4acd93785f (diff) |
Implement proper refund management
This means a few things:
1. We track refunds properly. Each invoice can be refunded once, but we now
also support partial refunds (which is important, as almost every refund we
make on a conference registration ends up being a partial one - if nothing
else it is deducted transaction fees).
2. We support API initiated refunds. That means that as long as the payment is
done using a supported provider (today that means Adyen and Paypal), the
complete invoice processing is done in the webapp, which means there is no
risk of getting the wrong numbers into the accounting (which has happened more
than once).
3. We now generate proper refund notices in PDF format for all refunds (both
automated and manual). The user receives this one as well as status updates
throughout the refund process.
Actual API refunds are handled by a new cronjob, to ensure that we don't end up
blocking the user if the API is slow. Initiating the refund puts it on the queue,
the cronjob sends it to the provider, and the notification from the provider
flags it as completed (and generates the refund notice).
Diffstat (limited to 'postgresqleu/adyen/util.py')
-rw-r--r-- | postgresqleu/adyen/util.py | 106 |
1 files changed, 92 insertions, 14 deletions
diff --git a/postgresqleu/adyen/util.py b/postgresqleu/adyen/util.py index 4f8cd6eb..eb34375b 100644 --- a/postgresqleu/adyen/util.py +++ b/postgresqleu/adyen/util.py @@ -4,6 +4,10 @@ from django.db import transaction from datetime import datetime, date +import json +import urllib2 +from base64 import standard_b64encode + from postgresqleu.mailqueue.util import send_simple_mail from postgresqleu.invoices.util import InvoiceManager from postgresqleu.invoices.models import Invoice, InvoicePaymentMethod @@ -157,25 +161,48 @@ def process_refund(notification): refund = Refund(notification=notification, transaction=ts, refund_amount=notification.amount) refund.save() - # Generate an open accounting record for this refund. - # We expect this happens so seldom that we can just deal with - # manually finishing off the accounting records. urls = [ "https://ca-live.adyen.com/ca/ca/accounts/showTx.shtml?pspReference=%s&txType=Payment&accountKey=MerchantAccount.%s" % (notification.pspReference, notification.merchantAccountCode), ] - accrows = [ - (settings.ACCOUNTING_ADYEN_REFUNDS_ACCOUNT, - "Refund of %s (transaction %s) " % (ts.notes, ts.pspReference), - -refund.refund_amount, - None), - ] - send_simple_mail(settings.INVOICE_SENDER_EMAIL, - settings.ADYEN_NOTIFICATION_RECEIVER, - 'Adyen refund received', - "A refund of %s%s for transaction %s was processed\n\nNOTE! You must complete the accounting system entry manually for refunds!" % (settings.CURRENCY_ABBREV, notification.amount, notification.originalReference)) + # API generated refund? + if notification.merchantReference.startswith('PGEUREFUND'): + # API generated + invoicerefundid = int(notification.merchantReference[10:]) + + # Get the correct method depending on how it was done + if ts.method == 'bankTransfer_IBAN': + method = InvoicePaymentMethod.objects.get(classname='postgresqleu.util.payment.adyen.AdyenBanktransfer') + else: + method = InvoicePaymentMethod.objects.get(classname='postgresqleu.util.payment.adyen.AdyenCreditcard') + + manager = InvoiceManager() + manager.complete_refund( + invoicerefundid, + refund.refund_amount, + 0, # we don't know the fee, it'll be generically booked + settings.ACCOUNTING_ADYEN_REFUNDS_ACCOUNT, + settings.ACCOUNTING_ADYEN_FEE_ACCOUNT, + urls, + method) - create_accounting_entry(date.today(), accrows, True, urls) + else: + # Generate an open accounting record for this refund. + # We expect this happens so seldom that we can just deal with + # manually finishing off the accounting records. + accrows = [ + (settings.ACCOUNTING_ADYEN_REFUNDS_ACCOUNT, + "Refund of %s (transaction %s) " % (ts.notes, ts.pspReference), + -refund.refund_amount, + None), + ] + + send_simple_mail(settings.INVOICE_SENDER_EMAIL, + settings.ADYEN_NOTIFICATION_RECEIVER, + 'Adyen refund received', + "A refund of %s%s for transaction %s was processed\n\nNOTE! You must complete the accounting system entry manually as it was not API generated!!" % (settings.CURRENCY_ABBREV, notification.amount, notification.originalReference)) + + create_accounting_entry(date.today(), accrows, True, urls) except TransactionStatus.DoesNotExist: send_simple_mail(settings.INVOICE_SENDER_EMAIL, @@ -323,3 +350,54 @@ def process_raw_adyen_notification(raw, POST): # Return that we've consumed the report outside the transaction, in # the unlikely event that the COMMIT is what failed return True + + + +# +# Accesspoints into the Adyen API +# +# Most of the API is off limits to us due to lack of PCI, but we can do a couple +# of important things like refunding. +# + +class AdyenAPI(object): + def refund_transaction(self, refundid, transreference, amount): + apiparam = { + 'merchantAccount': settings.ADYEN_MERCHANTACCOUNT, + 'modificationAmount': { + 'value': amount * 100, # "minor units", so cents! + 'currency': settings.CURRENCY_ISO, + }, + 'originalReference': transreference, + 'reference': 'PGEUREFUND{0}'.format(refundid), + } + + try: + r = self._api_call('pal/servlet/Payment/v12/refund', apiparam, '[refund-received]') + return r['pspReference'] + except Exception, e: + raise Exception("API call to refund transaction {0} (refund {1}) failed: {2}".format( + transreference, + refundid, + e)) + + + def _api_call(self, apiurl, apiparam, okresponse): + apijson = json.dumps(apiparam) + + req = urllib2.Request("{0}{1}".format(settings.ADYEN_APIBASEURL, apiurl)) + + req.add_header('Authorization', 'Basic {0}'.format( + standard_b64encode('{0}:{1}'.format(settings.ADYEN_WS_USER, settings.ADYEN_WS_PASSWORD)))) + req.add_header('Content-type', 'application/json') + u = urllib2.urlopen(req, apijson) + resp = u.read() + if u.getcode() != 200: + raise Exception("http response code {0}".format(u.getcode())) + u.close() + r = json.loads(resp) + + if r['response'] != okresponse: + raise Exception("response returned: {0}".format(r['response'])) + + return r |