From bbaf3ea9646b460f43e0738c02620314ff909058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Tue, 10 May 2016 17:55:54 +0200 Subject: [PATCH] Adding initial Epay integration - far from done. --- bornhack/settings/base.py | 6 ++ bornhack/settings/env.dist | 2 + tickets/admin.py | 2 + tickets/forms.py | 5 + .../migrations/0004_ticket_payment_method.py | 20 ++++ tickets/models.py | 41 ++++++- tickets/templates/tickets/epay_form.html | 27 +++++ tickets/templates/tickets/index.html | 6 +- .../tickets/{buy.html => order.html} | 0 tickets/urls.py | 10 +- tickets/views.py | 102 ++++++++++++++++-- 11 files changed, 210 insertions(+), 11 deletions(-) create mode 100644 tickets/migrations/0004_ticket_payment_method.py create mode 100644 tickets/templates/tickets/epay_form.html rename tickets/templates/tickets/{buy.html => order.html} (100%) diff --git a/bornhack/settings/base.py b/bornhack/settings/base.py index ae2c5978..816b468a 100644 --- a/bornhack/settings/base.py +++ b/bornhack/settings/base.py @@ -1,5 +1,8 @@ import os +import environ +env = environ.Env() +environ.Env.read_env() def local_dir(entry): return os.path.join( @@ -91,3 +94,6 @@ BOOTSTRAP3 = { 'jquery_url': '/static/js/jquery.min.js', 'javascript_url': '/static/js/bootstrap.min.js' } + +EPAY_MERCHANT_NUMBER = env('EPAY_MERCHANT_NUMBER') +EPAY_MD5_SECRET = env('EPAY_MD5_SECRET') diff --git a/bornhack/settings/env.dist b/bornhack/settings/env.dist index 23b99a18..dea3f432 100644 --- a/bornhack/settings/env.dist +++ b/bornhack/settings/env.dist @@ -7,3 +7,5 @@ EMAIL_HOST_USER='mymailuser' EMAIL_HOST_PASSWORD='mymailpassword' EMAIL_USE_TLS=True EMAIL_FROM='noreply@example.com' +EPAY_MERCHANT_NUMBER=something +EPAY_MD5_SECRET=something diff --git a/tickets/admin.py b/tickets/admin.py index 3cefc7a0..6519db95 100644 --- a/tickets/admin.py +++ b/tickets/admin.py @@ -8,12 +8,14 @@ class TicketAdmin(admin.ModelAdmin): list_display = [ 'user', 'ticket_type', + 'payment_method', 'paid', ] list_filter = [ 'paid', 'ticket_type', + 'payment_method', ] diff --git a/tickets/forms.py b/tickets/forms.py index 82412ddb..8cd05fee 100644 --- a/tickets/forms.py +++ b/tickets/forms.py @@ -8,9 +8,14 @@ class TicketForm(forms.ModelForm): model = Ticket fields = [ 'ticket_type', + 'payment_method', ] + widgets = { + 'payment_method': forms.RadioSelect() + } ticket_type = forms.ModelChoiceField( queryset=TicketType.objects.available() ) + diff --git a/tickets/migrations/0004_ticket_payment_method.py b/tickets/migrations/0004_ticket_payment_method.py new file mode 100644 index 00000000..ed92e3b2 --- /dev/null +++ b/tickets/migrations/0004_ticket_payment_method.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2016-05-08 11:21 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0003_auto_20160506_2016'), + ] + + operations = [ + migrations.AddField( + model_name='ticket', + name='payment_method', + field=models.CharField(choices=[('credit_card', 'Credit card'), ('altcoin', 'Altcoin'), ('bank_transfer', 'Bank transfer')], default='credit_card', max_length=50), + ), + ] diff --git a/tickets/models.py b/tickets/models.py index 595781a6..9ca7bf30 100644 --- a/tickets/models.py +++ b/tickets/models.py @@ -1,7 +1,8 @@ from django.db import models -from django.contrib.postgres.fields import DateTimeRangeField +from django.contrib.postgres.fields import DateTimeRangeField, JSONField from django.utils.translation import ugettext_lazy as _ from django.utils import timezone +from django.core.urlresolvers import reverse_lazy from bornhack.utils import CreatedUpdatedModel, UUIDModel @@ -31,12 +32,33 @@ class Ticket(CreatedUpdatedModel, UUIDModel): verbose_name=_('Ticket type'), ) + CREDIT_CARD = 'credit_card' + ALTCOIN = 'altcoin' + BANK_TRANSFER = 'bank_transfer' + + PAYMENT_METHODS = [ + (CREDIT_CARD, 'Credit card'), + (ALTCOIN, 'Altcoin'), + (BANK_TRANSFER, 'Bank transfer'), + ] + + payment_method = models.CharField( + max_length=50, + choices=PAYMENT_METHODS, + default=CREDIT_CARD + ) + def __str__(self): return '{} ({})'.format( self.ticket_type.name, self.ticket_type.camp ) + def get_absolute_url(self): + return reverse_lazy('ticket:detail', kwargs={ + 'pk': self.pk + }) + class TicketType(CreatedUpdatedModel, UUIDModel): class Meta: @@ -74,3 +96,20 @@ class TicketType(CreatedUpdatedModel, UUIDModel): def is_available(self): now = timezone.now() return now in self.available_in + + +class EpayCallback(CreatedUpdatedModel, UUIDModel): + class Meta: + verbose_name = 'Epay Callback' + verbose_name_plural = 'Epay Callbacks' + payload = JSONField() + + +class EpayPayment(CreatedUpdatedModel, UUIDModel): + class Meta: + verbose_name = 'Epay Payment' + verbose_name_plural = 'Epay Payments' + + ticket = models.OneToOneField('tickets.Ticket') + callback = models.ForeignKey('tickets.EpayCallback') + txnid = models.IntegerField() diff --git a/tickets/templates/tickets/epay_form.html b/tickets/templates/tickets/epay_form.html new file mode 100644 index 00000000..d88988d8 --- /dev/null +++ b/tickets/templates/tickets/epay_form.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block content %} +
+ + + + +
+{% endblock content %} + diff --git a/tickets/templates/tickets/index.html b/tickets/templates/tickets/index.html index 86d131a2..a4db6020 100644 --- a/tickets/templates/tickets/index.html +++ b/tickets/templates/tickets/index.html @@ -36,7 +36,9 @@ Here you can see the different ticket types, their prices and availability. {% endif %} {% if ticket_type.is_available %} - Buy + + Order + {% else %} N/A {% endif %} @@ -63,7 +65,7 @@ Here you can see the different ticket types, their prices and availability. {% endfor %} - Buy tickets + Order tickets {% else %} Sign up or diff --git a/tickets/templates/tickets/buy.html b/tickets/templates/tickets/order.html similarity index 100% rename from tickets/templates/tickets/buy.html rename to tickets/templates/tickets/order.html diff --git a/tickets/urls.py b/tickets/urls.py index ac577231..faca5bea 100644 --- a/tickets/urls.py +++ b/tickets/urls.py @@ -1,9 +1,15 @@ from django.conf.urls import url -from .views import BuyTicketView, TicketIndexView, TicketDetailView +from .views import ( + TicketIndexView, + TicketOrderView, + TicketDetailView, + EpayView, +) urlpatterns = [ - url(r'buy/$', BuyTicketView.as_view(), name='buy'), + url(r'order/$', TicketOrderView.as_view(), name='order'), + url(r'pay/credit_card/(?P[a-zA-Z0-9\-]+)/$', EpayView.as_view(), name='epay_form'), url( r'detail/(?P[a-zA-Z0-9\-]+)/$', TicketDetailView.as_view(), diff --git a/tickets/views.py b/tickets/views.py index 29b1193e..a0431910 100644 --- a/tickets/views.py +++ b/tickets/views.py @@ -1,12 +1,15 @@ +import hashlib + from django.http import HttpResponseRedirect, Http404 -from django.shortcuts import render -from django.views.generic import CreateView, TemplateView, DetailView +from django.views.generic import CreateView, TemplateView, DetailView, View from django.core.urlresolvers import reverse_lazy +from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpResponse from camps.models import Camp -from .models import Ticket, TicketType +from .models import Ticket, TicketType, EpayPayment, EpayCallback from .forms import TicketForm @@ -33,13 +36,13 @@ class TicketDetailView(LoginRequiredMixin, CampTicketSaleCheck, DetailView): context_object_name = 'ticket' -class BuyTicketView(LoginRequiredMixin, CampTicketSaleCheck, CreateView): +class TicketOrderView(LoginRequiredMixin, CampTicketSaleCheck, CreateView): model = Ticket - template_name = "tickets/buy.html" + template_name = "tickets/order.html" form_class = TicketForm def get_form_kwargs(self): - kwargs = super(BuyTicketView, self).get_form_kwargs() + kwargs = super(TicketOrderView, self).get_form_kwargs() ticket_type = self.request.GET.get('ticket_type', None) if ticket_type: kwargs['initial'] = { @@ -52,9 +55,96 @@ class BuyTicketView(LoginRequiredMixin, CampTicketSaleCheck, CreateView): instance = form.save(commit=False) instance.user = self.request.user instance.save() + + if instance.payment_method == Ticket.ALTCOIN: + return HttpResponse('Altcoin') + + if instance.payment_method == Ticket.CREDIT_CARD: + return HttpResponseRedirect( + reverse_lazy('tickets:epay_form', kwargs={ + 'ticket_id': str(instance.pk) + }) + ) + return HttpResponseRedirect( reverse_lazy('tickets:detail', kwargs={ 'pk': str(instance.pk) }) ) + +class EpayView(TemplateView): + template_name = 'tickets/epay_form.html' + + def get_context_data(self, **kwargs): + ticket = Ticket.objects.get(pk=kwargs).select_related('ticket_type') + accept_url = ticket.get_absolute_url() + amount = ticket.ticket_type.price * 100 + order_id = str(ticket.pk) + description = str(ticket.user.pk) + + hashstring = ( + '{merchantnumber}{description}11{amount}DKK' + '{order_id}{accept_url}{md5_secret}' + ).format( + settings.EPAY_MERCHANT_NUMBER, + description, + str(amount), + str(order_id), + accept_url, + settings.EPAY_MD5_SECRET, + ) + epay_hash = hashlib.md5(hashstring).hexdigest() + + context = super(EpayView, self).get_context_data(**kwargs) + context['merchant_number'] = settings.EPAY_MERCHANT_NUMBER + context['description'] = description + context['order_id'] = order_id + context['accept_url'] = accept_url + context['amount'] = amount + context['epay_hash'] = epay_hash + return context + + +class EpayCallbackView(View): + + def get(self, request, **kwargs): + + callback = EpayCallback.objects.create( + payload=request.GET + ) + + if 'orderid' in request.GET: + ticket = Ticket.objects.get(pk=request.GET.get('order_id')) + query = dict( + map( + lambda x: tuple(x.split('=')), + request.META['QUERY_STRING'].split('&') + ) + ) + + hashstring = ( + '{merchantnumber}{description}11{amount}DKK' + '{order_id}{accept_url}{md5_secret}' + ).format( + query.get('merchantnumber'), + query.get('description'), + query.get('amount'), + query.get('order_id'), + query.get('accept_url'), + settings.EPAY_MD5_SECRET, + ) + epay_hash = hashlib.md5(hashstring).hexdigest() + + if not epay_hash == request.GET['hash']: + return HttpResponse(status=400) + + EpayPayment.objects.create( + ticket=ticket, + callback=callback, + txnid=request.GET['txnid'], + ) + else: + return HttpResponse(status=400) + + return HttpResponse('OK')