diff --git a/bornhack/settings/development.py b/bornhack/settings/development.py index f0dc71bc..aad4a1b9 100644 --- a/bornhack/settings/development.py +++ b/bornhack/settings/development.py @@ -18,3 +18,5 @@ DATABASES = { EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' LETTERHEAD_PDF_PATH = os.path.join(local_dir('static_src'), 'pdf', 'bornhack_2016_test_letterhead.pdf') +COINIFY_API_KEY = '{{ coinify_api_key }}' +COINIFY_API_SECRET = '{{ coinify_api_secret }}' diff --git a/shop/models.py b/shop/models.py index b47988a7..b9bd351b 100644 --- a/shop/models.py +++ b/shop/models.py @@ -91,6 +91,9 @@ class Order(CreatedUpdatedModel): def get_epay_cancel_url(self, request): return 'https://' + request.get_host() + str(reverse_lazy('shop:order_detail', kwargs={'pk': self.pk})) + def get_epay_callback_url(self, request): + return 'https://' + request.get_host() + str(reverse_lazy('shop:epay_callback', kwargs={'pk': self.pk})) + @property def description(self): return "BornHack 2016 order #%s" % self.pk @@ -207,3 +210,15 @@ class Invoice(CreatedUpdatedModel): self.sent_to_customer, ) + +class CoinifyAPIInvoice(CreatedUpdatedModel): + invoicejson = JSONField() + order = models.OneToOneField('shop.Order') + + +class CoinifyCallback(CreatedUpdatedModel): + payload = JSONField() + + def __str__(self): + return 'callback at %s' % self.created + diff --git a/shop/urls.py b/shop/urls.py index 88e107d5..977d3d43 100644 --- a/shop/urls.py +++ b/shop/urls.py @@ -3,12 +3,19 @@ from views import * urlpatterns = [ url(r'^$', ShopIndexView.as_view(), name='index'), + url(r'products/(?P[-_\w+]+)/$', ProductDetailView.as_view(), name='product_detail'), + url(r'orders/$', OrderListView.as_view(), name='order_list'), url(r'orders/(?P[0-9]+)/$', OrderDetailView.as_view(), name='order_detail'), + url(r'orders/(?P[0-9]+)/pay/creditcard/$', EpayFormView.as_view(), name='epay_form'), + url(r'orders/(?P[0-9]+)/pay/creditcard/callback/$',EpayCallbackView.as_view(), name='epay_callback'), url(r'orders/(?P[0-9]+)/pay/creditcard/thanks/$', EpayThanksView.as_view(), name='epay_thanks'), + url(r'orders/(?P[0-9]+)/pay/blockchain/$', CoinifyRedirectView.as_view(), name='coinify_pay'), + url(r'orders/(?P[0-9]+)/pay/blockchain/callback/$', CoinifyCallbackView.as_view(), name='coinify_callback'), + url(r'orders/(?P[0-9]+)/pay/blockchain/thanks/$', CoinifyThanksView.as_view(), name='coinify_thanks'), + url(r'orders/(?P[0-9]+)/pay/banktransfer/$', BankTransferView.as_view(), name='bank_transfer'), - url(r'epay_callback/', EpayCallbackView.as_view(), name='epay_callback'), ] diff --git a/shop/views.py b/shop/views.py index 02bdb0ec..d9ce3aa5 100644 --- a/shop/views.py +++ b/shop/views.py @@ -3,7 +3,7 @@ from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.core.urlresolvers import reverse_lazy from django.db.models import Count, F -from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest, Http404 from django.shortcuts import get_object_or_404 from django.views.generic import ( View, @@ -22,11 +22,13 @@ from shop.models import ( ProductCategory, EpayCallback, EpayPayment, + CoinifyAPIInvoice, ) from .forms import AddToOrderForm from .epay import calculate_epay_hash, validate_epay_callback from collections import OrderedDict from vendor.coinify_api import CoinifyAPI +from vendor.coinify_callback import CoinifyCallback class EnsureUserOwnsOrderMixin(SingleObjectMixin): @@ -234,49 +236,6 @@ class ProductDetailView(LoginRequiredMixin, FormView, DetailView): return Order.objects.get(user=self.request.user, open__isnull=False).get_absolute_url() -class CoinifyRedirectView(LoginRequiredMixin, EnsureUserOwnsOrderMixin, EnsureUnpaidOrderMixin, EnsureClosedOrderMixin, EnsureOrderHasProductsMixin, DetailView): - model = Order - template_name = 'coinify_redirect.html' - - def get_context_data(self, **kwargs): - order = self.get_object() - context = super(CoinifyRedirectView, self).get_context_data(**kwargs) - context['order'] = order - - # Initiate coinify API and create invoice - coinifyapi = CoinifyAPI( - settings.COINIFY_API_KEY, - settings.COINIFY_API_SECRET - ) - response = coinifyapi.invoice_create( - amount, - currency, - plugin_name='BornHack 2016 webshop', - plugin_version='1.0', - description='BornHack 2016 order id #%s' % order.id, - callback_url=reverse_lazy( - 'shop:coinfy_callback', - kwargs={'orderid': order.id} - ), - return_url=reverse_lazy( - 'shop:order_paid', - kwargs={'orderid': order.id} - ), - ) - - if not response['success']: - api_error = response['error'] - print "API error: %s (%s)" % ( - api_error['message'], - api_error['code'] - ) - - invoice = response['data'] - # change this to pass only needed data when we get that far - context['invoice'] = invoice - return context - - class EpayFormView(LoginRequiredMixin, EnsureUserOwnsOrderMixin, EnsureUnpaidOrderMixin, EnsureClosedOrderMixin, EnsureOrderHasProductsMixin, DetailView): model = Order template_name = 'epay_form.html' @@ -285,6 +244,7 @@ class EpayFormView(LoginRequiredMixin, EnsureUserOwnsOrderMixin, EnsureUnpaidOrd order = self.get_object() accept_url = order.get_epay_accept_url(self.request) cancel_url = order.get_epay_cancel_url(self.request) + callback_url = order.get_epay_callback_url(self.request) amount = order.total * 100 epay_hash = calculate_epay_hash(order, self.request) @@ -296,6 +256,7 @@ class EpayFormView(LoginRequiredMixin, EnsureUserOwnsOrderMixin, EnsureUnpaidOrd context['order_id'] = order.pk context['accept_url'] = accept_url context['cancel_url'] = cancel_url + context['callback_url'] = callback_url context['epay_hash'] = epay_hash return context @@ -364,3 +325,89 @@ class BankTransferView(LoginRequiredMixin, EnsureUserOwnsOrderMixin, EnsureUnpai model = Order template_name = 'bank_transfer.html' + +class CoinifyRedirectView(LoginRequiredMixin, EnsureUserOwnsOrderMixin, EnsureUnpaidOrderMixin, EnsureClosedOrderMixin, EnsureOrderHasProductsMixin, DetailView): + model = Order + template_name = 'coinify_redirect.html' + + def get_context_data(self, **kwargs): + order = self.get_object() + context = super(CoinifyRedirectView, self).get_context_data(**kwargs) + + if hasattr(order, 'coinifyapiinvoice'): + # we already have an invoice for this order, just redirect + context['redirecturl'] == order.coinifyapiinvoice.payload['data']['payment_url'] + else: + # Initiate coinify API and create invoice + coinifyapi = CoinifyAPI( + settings.COINIFY_API_KEY, + settings.COINIFY_API_SECRET + ) + response = coinifyapi.invoice_create( + order.total, + 'DKK', + plugin_name='BornHack 2016 webshop', + plugin_version='1.0', + description='BornHack 2016 order id #%s' % order.id, + callback_url=reverse_lazy( + 'shop:coinfy_callback', + kwargs={'orderid': order.id} + ), + return_url=reverse_lazy( + 'shop:coinify_thanks', + kwargs={'orderid': order.id} + ), + ) + + if not response['success']: + api_error = response['error'] + print "API error: %s (%s)" % ( + api_error['message'], + api_error['code'] + ) + else: + # save this coinify invoice + CoinifyAPIInvoice.objects.create( + invoicejson = response['data'], + order = order, + ) + context['redirecturl'] == response['data']['payment_url'] + return context + + +class CoinifyCallbackView(View): + def post(self, request, *args, **kwargs): + # Get the signature from the HTTP or email headers + signature = request.META['HTTP_X_COINIFY_CALLBACK_SIGNATURE'] + sdk = CoinifyCallback( + settings.COINIFY_API_KEY, + settings.COINIFY_API_SECRET + ) + + if sdk.validate_callback(request.body, signature): + # callback is valid, save it to db + callbackobject = CoinifyCallback.objects.create( + payload=request.body + ) + + # parse json + callbackjson = json.loads(request.body) + if callbackjson['event'] == 'invoice_state_change' or callbackjson['event'] == 'invoice_manual_resend': + # get invoice from db + try: + invoice = CoinifyAPIInvoice.objects.get(id=callbackjson['data']['id']) + except CoinifyAPIInvoice.DoesNotExist: + return HttpResponseBadRequest('bad invoice id') + + # save new invoice payload + invoice.payload = callbackjson['data'] + invoice.save() + + # so, is the invoice paid now? + if callbackjson['data']['state'] == 'complete': + invoice.order.mark_as_paid() + else: + HttpResponseBadRequest('unsupported event') + else: + HttpResponseBadRequest('something is fucky') +