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 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()
|
||||
|
|
|
@ -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,67 +565,34 @@ 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()
|
||||
|
||||
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:
|
||||
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':
|
||||
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):
|
||||
model = Order
|
||||
|
|
Loading…
Reference in a new issue