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:
parent
469b708782
commit
8644d4ba2f
148
src/shop/coinify.py
Normal file
148
src/shop/coinify.py
Normal 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
|
||||||
|
|
21
src/shop/migrations/0042_auto_20170507_1000.py
Normal file
21
src/shop/migrations/0042_auto_20170507_1000.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
36
src/shop/migrations/0043_auto_20170507_1309.py
Normal file
36
src/shop/migrations/0043_auto_20170507_1309.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
20
src/shop/migrations/0044_coinifyapiinvoice_coinify_id.py
Normal file
20
src/shop/migrations/0044_coinifyapiinvoice_coinify_id.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
20
src/shop/migrations/0045_auto_20170507_1648.py
Normal file
20
src/shop/migrations/0045_auto_20170507_1648.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
21
src/shop/migrations/0046_coinifyapirequest_method.py
Normal file
21
src/shop/migrations/0046_coinifyapirequest_method.py
Normal 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,
|
||||||
|
),
|
||||||
|
]
|
|
@ -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()
|
||||||
|
|
|
@ -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,66 +565,33 @@ 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()
|
||||||
|
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':
|
if callbackobject.payload['event'] == 'invoice_state_change' or callbackobject.payload['event'] == 'invoice_manual_resend':
|
||||||
# find coinify invoice in db
|
coinifyinvoice = process_coinify_invoice_json(invoicejson, self.get_object())
|
||||||
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')
|
return HttpResponse('OK')
|
||||||
else:
|
else:
|
||||||
logger.error("unsupported callback event %s" % callbackobject.payload['event'])
|
logger.error("unsupported callback event %s" % callbackobject.payload['event'])
|
||||||
return HttpResponseBadRequest('unsupported event')
|
return HttpResponseBadRequest('unsupported event')
|
||||||
else:
|
|
||||||
logger.error("invalid coinify callback detected")
|
|
||||||
return HttpResponseBadRequest('something is fucky')
|
|
||||||
|
|
||||||
|
|
||||||
class CoinifyThanksView(LoginRequiredMixin, EnsureUserOwnsOrderMixin, EnsureClosedOrderMixin, DetailView):
|
class CoinifyThanksView(LoginRequiredMixin, EnsureUserOwnsOrderMixin, EnsureClosedOrderMixin, DetailView):
|
||||||
|
|
Loading…
Reference in a new issue