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 datetime import timedelta
from unidecode import unidecode
from django.utils.dateparse import parse_datetime
from django.utils import timezone
class CustomOrder(CreatedUpdatedModel):
@ -407,6 +409,10 @@ class CoinifyAPIInvoice(CreatedUpdatedModel):
def __str__(self):
return "coinifyinvoice for order #%s" % self.order.id
@property
def expired(self):
return parse_datetime(self.invoicejson['expire_time']) < timezone.now()
class CoinifyAPICallback(CreatedUpdatedModel):
headers = JSONField()

View file

@ -534,51 +534,21 @@ class CoinifyRedirectView(LoginRequiredMixin, EnsureUserOwnsOrderMixin, EnsureUn
def dispatch(self, request, *args, **kwargs):
order = self.get_object()
# check if we already have a coinifyinvoice for this order
if hasattr(order, 'coinifyapiinvoice'):
# we already have a coinifyinvoice for this order,
# check if it expired
if parse_datetime(order.coinifyapiinvoice.invoicejson['expire_time']) < timezone.now():
# this coinifyinvoice expired, delete it
logger.warning("deleting expired coinifyinvoice id %s" % order.coinifyapiinvoice.invoicejson['id'])
order.coinifyapiinvoice.delete()
order.refresh_from_db()
# we already have a coinifyinvoice for this order, check if it expired
if order.coinifyapiinvoice.expired:
# this coinifyinvoice expired, we need a new one
logger.warning("coinifyinvoice id %s expired" % order.coinifyapiinvoice.invoicejson['id'])
order.coinifyapiinvoice = None
# create a new coinify invoice if needed
if not hasattr(order, 'coinifyapiinvoice'):
# Initiate coinify API
coinifyapi = CoinifyAPI(
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']
))
coinifyinvoice = create_coinify_invoice(order)
if not coinifyinvoice:
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}))
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(
request, *args, **kwargs
)
@ -595,66 +565,33 @@ class CoinifyCallbackView(SingleObjectMixin, View):
return super(CoinifyCallbackView, self).dispatch(*args, **kwargs)
def post(self, request, *args, **kwargs):
# Get the signature from the HTTP headers
signature = request.META['HTTP_X_COINIFY_CALLBACK_SIGNATURE']
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()
)
# save callback and parse json payload
callbackobject = save_coinify_callback(request)
# do we have a json body?
if not parsed:
if not callbackobject.payload:
# no, return an error
logger.error("unable to parse JSON body in callback for order %s" % callbackobject.order.id)
return HttpResponseBadRequest('unable to parse json')
# initiate SDK
sdk = CoinifyCallback(settings.COINIFY_IPN_SECRET.encode('utf-8'))
# 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
callbackobject.valid=True
callbackobject.save()
else:
logger.error("invalid coinify callback detected")
return HttpResponseBadRequest('something is fucky')
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
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')
else:
logger.error("invalid coinify callback detected")
return HttpResponseBadRequest('something is fucky')
class CoinifyThanksView(LoginRequiredMixin, EnsureUserOwnsOrderMixin, EnsureClosedOrderMixin, DetailView):