Coinify multiinvoice fix (#126)

* rework coinify api stuff, work in progress

* fix imports

* indentation

* rework coinify api stuff, work in progress

* fix imports

* indentation
This commit is contained in:
Thomas Steen Rasmussen 2017-05-22 18:03:09 +02:00 committed by GitHub
parent 469b708782
commit 8644d4ba2f
8 changed files with 295 additions and 86 deletions

148
src/shop/coinify.py Normal file
View file

@ -0,0 +1,148 @@
from vendor.coinify.coinify_api import CoinifyAPI
from vendor.coinify.coinify_callback import CoinifyCallback
from .models import CoinifyAPIRequest, CoinifyAPIInvoice
import json, logging
logger = logging.getLogger("bornhack.%s" % __name__)
def process_coinify_invoice_json(invoicejson, order):
# create or update the invoice object in our database
coinifyinvoice, created = CoinifyAPIInvoice.objects.update_or_create(
coinify_id=invoicejson['id'],
order=order,
defaults={
invoicejson: invoicejson
},
)
# if the order is paid in full call the mark as paid method now
if invoicejson['state'] == 'complete':
coinifyinvoice.order.mark_as_paid()
return coinifyinvoice
def save_coinify_callback(request):
# first make a dict with all HTTP_ headers
headerdict = {}
for key, value in list(request.META.items()):
if key[:5] == 'HTTP_':
headerdict[key[5:]] = value
# now attempt to parse json
try:
parsed = json.loads(request.body.decode('utf-8'))
except Exception as E:
parsed = ''
# save this callback to db
callbackobject = CoinifyAPICallback.objects.create(
headers=headerdict,
body=request.body,
payload=parsed,
order=self.get_object(),
)
return callbackobject
def coinify_api_request(api_method, order, **kwargs):
# Initiate coinify API
coinifyapi = CoinifyAPI(
settings.COINIFY_API_KEY,
settings.COINIFY_API_SECRET
)
# is this a supported method?
if not hasattr(coinifyapi, api_method):
logger.error("coinify api method not supported" % api_method)
return False
# get and run the API call using the SDK
method = getattr(coinifyapi, api_method)
# catch requests exceptions as described in https://github.com/CoinifySoftware/python-sdk#catching-errors and
# http://docs.python-requests.org/en/latest/user/quickstart/#errors-and-exceptions
try:
response = method(**kwargs)
except requests.exceptions.RequestException as E:
logger.error("requests exception during coinify api request: %s" % E)
return False
# save this API request to the database
req = CoinifyAPIRequest.objects.create(
order=order,
method=api_method,
payload=json.dumps(invoicedict),
response=json.dumps(response),
)
logger.debug("saved coinify api request %s in db" % req.id)
return req
def handle_coinify_api_response(req, order):
if req.method == 'invoice_create' or req.method == 'invoice_get':
# Parse api response
if req.response['success']:
# save this new coinify invoice to the DB
coinifyinvoice = process_coinify_invoice_json(
invoicejson = req.response['data'],
order = order,
)
return coinifyinvoice
else:
api_error = req.response['error']
logger.error("coinify API error: %s (%s)" % (
api_error['message'],
api_error['code']
))
return False
else:
logger.error("coinify api method not supported" % req.method)
return False
################### API CALLS ################################################
def get_coinify_invoice(coinify_invoiceid, order):
# put args for API request together
invoicedict = {
'invoice_id': coinify_invoiceid
}
# perform the api request
req = coinify_api_request(
api_method='invoice_get',
**invoicedict
)
coinifyinvoice = handle_coinify_api_response(req, order)
return coinifyinvoice
def create_coinify_invoice(order):
# put args for API request together
invoicedict = {
'amount': float(order.total),
'currency': 'DKK',
'plugin_name': 'BornHack webshop',
'plugin_version': '1.0',
'description': 'BornHack order id #%s' % order.id,
'callback_url': order.get_coinify_callback_url(request),
'return_url': order.get_coinify_thanks_url(request),
'cancel_url': order.get_cancel_url(request),
}
# perform the API request
req = coinify_api_request(
api_method='invoice_create',
order=order,
**invoicedict
)
coinifyinvoice = handle_coinify_api_response(req, order)
return coinifyinvoice

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-07 08:00
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('shop', '0041_auto_20170408_1104'),
]
operations = [
migrations.AlterField(
model_name='coinifyapiinvoice',
name='order',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='coinify_api_invoices', to='shop.Order'),
),
]

View file

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-07 11:09
from __future__ import unicode_literals
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('shop', '0042_auto_20170507_1000'),
]
operations = [
migrations.CreateModel(
name='CoinifyAPIRequest',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('payload', django.contrib.postgres.fields.jsonb.JSONField()),
('response', models.TextField()),
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='coinify_api_requests', to='shop.Order')),
],
options={
'abstract': False,
},
),
migrations.AlterField(
model_name='coinifyapicallback',
name='order',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='coinify_api_callbacks', to='shop.Order'),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-07 13:41
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0043_auto_20170507_1309'),
]
operations = [
migrations.AddField(
model_name='coinifyapiinvoice',
name='coinify_id',
field=models.IntegerField(null=True),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-07 14:48
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('shop', '0044_coinifyapiinvoice_coinify_id'),
]
operations = [
migrations.RenameField(
model_name='coinifyapicallback',
old_name='valid',
new_name='authenticated',
),
]

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-07 17:50
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0045_auto_20170507_1648'),
]
operations = [
migrations.AddField(
model_name='coinifyapirequest',
name='method',
field=models.CharField(default='', max_length=100),
preserve_default=False,
),
]

View file

@ -14,6 +14,8 @@ import hashlib, io, base64, qrcode
from decimal import Decimal from decimal import Decimal
from datetime import timedelta from datetime import timedelta
from unidecode import unidecode from unidecode import unidecode
from django.utils.dateparse import parse_datetime
from django.utils import timezone
class CustomOrder(CreatedUpdatedModel): class CustomOrder(CreatedUpdatedModel):
@ -407,6 +409,10 @@ class CoinifyAPIInvoice(CreatedUpdatedModel):
def __str__(self): def __str__(self):
return "coinifyinvoice for order #%s" % self.order.id return "coinifyinvoice for order #%s" % self.order.id
@property
def expired(self):
return parse_datetime(self.invoicejson['expire_time']) < timezone.now()
class CoinifyAPICallback(CreatedUpdatedModel): class CoinifyAPICallback(CreatedUpdatedModel):
headers = JSONField() headers = JSONField()

View file

@ -534,51 +534,21 @@ class CoinifyRedirectView(LoginRequiredMixin, EnsureUserOwnsOrderMixin, EnsureUn
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
order = self.get_object() order = self.get_object()
# check if we already have a coinifyinvoice for this order
if hasattr(order, 'coinifyapiinvoice'): if hasattr(order, 'coinifyapiinvoice'):
# we already have a coinifyinvoice for this order, # we already have a coinifyinvoice for this order, check if it expired
# check if it expired if order.coinifyapiinvoice.expired:
if parse_datetime(order.coinifyapiinvoice.invoicejson['expire_time']) < timezone.now(): # this coinifyinvoice expired, we need a new one
# this coinifyinvoice expired, delete it logger.warning("coinifyinvoice id %s expired" % order.coinifyapiinvoice.invoicejson['id'])
logger.warning("deleting expired coinifyinvoice id %s" % order.coinifyapiinvoice.invoicejson['id']) order.coinifyapiinvoice = None
order.coinifyapiinvoice.delete()
order.refresh_from_db()
# create a new coinify invoice if needed # create a new coinify invoice if needed
if not hasattr(order, 'coinifyapiinvoice'): if not hasattr(order, 'coinifyapiinvoice'):
# Initiate coinify API coinifyinvoice = create_coinify_invoice(order)
coinifyapi = CoinifyAPI( if not coinifyinvoice:
settings.COINIFY_API_KEY,
settings.COINIFY_API_SECRET
)
# create coinify API
response = coinifyapi.invoice_create(
float(order.total),
'DKK',
plugin_name='BornHack webshop',
plugin_version='1.0',
description='BornHack order id #%s' % order.id,
callback_url=order.get_coinify_callback_url(request),
return_url=order.get_coinify_thanks_url(request),
cancel_url=order.get_cancel_url(request),
)
# Parse response
if not response['success']:
api_error = response['error']
logger.error("API error: %s (%s)" % (
api_error['message'],
api_error['code']
))
messages.error(request, "There was a problem with the payment provider. Please try again later") messages.error(request, "There was a problem with the payment provider. Please try again later")
return HttpResponseRedirect(reverse_lazy('shop:order_detail', kwargs={'pk': self.get_object().pk})) return HttpResponseRedirect(reverse_lazy('shop:order_detail', kwargs={'pk': self.get_object().pk}))
else:
# save this coinify invoice
coinifyinvoice = CoinifyAPIInvoice.objects.create(
invoicejson = response['data'],
order = order,
)
logger.info("created new coinifyinvoice id %s" % coinifyinvoice.invoicejson['id'])
return super(CoinifyRedirectView, self).dispatch( return super(CoinifyRedirectView, self).dispatch(
request, *args, **kwargs request, *args, **kwargs
) )
@ -595,67 +565,34 @@ class CoinifyCallbackView(SingleObjectMixin, View):
return super(CoinifyCallbackView, self).dispatch(*args, **kwargs) return super(CoinifyCallbackView, self).dispatch(*args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
# Get the signature from the HTTP headers # save callback and parse json payload
signature = request.META['HTTP_X_COINIFY_CALLBACK_SIGNATURE'] callbackobject = save_coinify_callback(request)
sdk = CoinifyCallback(settings.COINIFY_IPN_SECRET.encode('utf-8'))
# make a dict with all HTTP_ headers
headerdict = {}
for key, value in list(request.META.items()):
if key[:5] == 'HTTP_':
headerdict[key[5:]] = value
# attempt to parse json
try:
parsed = json.loads(request.body.decode('utf-8'))
except Exception as E:
parsed = ''
# save callback to db
callbackobject = CoinifyAPICallback.objects.create(
headers=headerdict,
body=request.body,
payload=parsed,
order=self.get_object()
)
# do we have a json body? # do we have a json body?
if not parsed: if not callbackobject.payload:
# no, return an error # no, return an error
logger.error("unable to parse JSON body in callback for order %s" % callbackobject.order.id) logger.error("unable to parse JSON body in callback for order %s" % callbackobject.order.id)
return HttpResponseBadRequest('unable to parse json') return HttpResponseBadRequest('unable to parse json')
# initiate SDK
sdk = CoinifyCallback(settings.COINIFY_IPN_SECRET.encode('utf-8'))
# attemt to validate the callbackc # attemt to validate the callbackc
if sdk.validate_callback(request.body, signature): if sdk.validate_callback(request.body, request.META['HTTP_X_COINIFY_CALLBACK_SIGNATURE']):
# mark callback as valid in db # mark callback as valid in db
callbackobject.valid=True callbackobject.valid=True
callbackobject.save() callbackobject.save()
if callbackobject.payload['event'] == 'invoice_state_change' or callbackobject.payload['event'] == 'invoice_manual_resend':
# find coinify invoice in db
try:
coinifyinvoice = CoinifyAPIInvoice.objects.get(invoicejson__id=callbackobject.payload['data']['id'])
except CoinifyAPIInvoice.DoesNotExist:
logger.error("unable to find CoinifyAPIInvoice with id %s" % callbackobject.payload['data']['id'])
return HttpResponseBadRequest('bad coinifyinvoice id')
# save new coinifyinvoice payload
coinifyinvoice.invoicejson = callbackobject.payload['data']
coinifyinvoice.save()
# so, is the order paid in full now?
if callbackobject.payload['data']['state'] == 'complete':
coinifyinvoice.order.mark_as_paid()
# return 200 OK
return HttpResponse('OK')
else:
logger.error("unsupported callback event %s" % callbackobject.payload['event'])
return HttpResponseBadRequest('unsupported event')
else: else:
logger.error("invalid coinify callback detected") logger.error("invalid coinify callback detected")
return HttpResponseBadRequest('something is fucky') return HttpResponseBadRequest('something is fucky')
if callbackobject.payload['event'] == 'invoice_state_change' or callbackobject.payload['event'] == 'invoice_manual_resend':
coinifyinvoice = process_coinify_invoice_json(invoicejson, self.get_object())
return HttpResponse('OK')
else:
logger.error("unsupported callback event %s" % callbackobject.payload['event'])
return HttpResponseBadRequest('unsupported event')
class CoinifyThanksView(LoginRequiredMixin, EnsureUserOwnsOrderMixin, EnsureClosedOrderMixin, DetailView): class CoinifyThanksView(LoginRequiredMixin, EnsureUserOwnsOrderMixin, EnsureClosedOrderMixin, DetailView):
model = Order model = Order