admin.site.register(models.Plan, PlanAdmin)
-class PaymentInline(admin.TabularInline):
- model = models.Payment
+class PayUOrderInline(admin.TabularInline):
+ model = models.PayUOrder
extra = 0
- readonly_fields = ['payed_at']
+ show_change_link = True
+
+
+class PayUCardTokenInline(admin.TabularInline):
+ model = models.PayUCardToken
+ extra = 0
+ show_change_link = True
class ScheduleAdmin(admin.ModelAdmin):
- list_display = ['email', 'started_at', 'expires_at', 'plan', 'amount', 'is_active', 'is_cancelled']
+ list_display = ['email', 'started_at', 'expires_at', 'plan', 'amount', 'is_cancelled']
list_search = ['email']
- list_filter = ['is_active', 'is_cancelled']
+ list_filter = ['is_cancelled']
date_hierarchy = 'started_at'
raw_id_fields = ['membership']
- inlines = [PaymentInline]
+ inlines = [PayUOrderInline, PayUCardTokenInline]
admin.site.register(models.Schedule, ScheduleAdmin)
-class PaymentAdmin(admin.ModelAdmin):
- list_display = ['payed_at', 'schedule']
-
-admin.site.register(models.Payment, PaymentAdmin)
-
+class ScheduleInline(admin.TabularInline):
+ model = models.Schedule
+ extra = 0
+ show_change_link = True
class MembershipAdmin(admin.ModelAdmin):
list_display = ['user']
raw_id_fields = ['user']
search_fields = ['user__username', 'user__email']
+ inlines = [ScheduleInline]
admin.site.register(models.Membership, MembershipAdmin)
admin.site.register(models.ReminderEmail, TranslationAdmin)
+
+
+admin.site.register(models.PayUNotification)
-# -*- coding: utf-8
from django import forms
from . import models
from .payment_methods import method_by_slug
+from .payu.forms import CardTokenForm
class ScheduleForm(forms.ModelForm):
'method': forms.RadioSelect,
}
- def __init__(self, *args, **kwargs):
+ def __init__(self, *args, request=None, **kwargs):
super(ScheduleForm, self).__init__(*args, **kwargs)
+ self.request = request
self.fields['plan'].empty_label = None
def clean(self):
if cleaned_data['amount'] < cleaned_data['plan'].min_amount:
self.add_error('amount', 'Minimalna kwota dla tego planu to %d zł.' % cleaned_data['plan'].min_amount)
+
+class PayUCardTokenForm(CardTokenForm):
+ def get_queryset(self, view):
+ return view.get_schedule().payucardtoken_set
def get_active_schedule(user):
if not user.is_authenticated:
return None
- return Schedule.objects.filter(membership__user=user, is_active=True).exclude(expires_at__lt=now()).first()
+ return Schedule.objects.filter(membership__user=user, expires_at__gt=now()).first()
--- /dev/null
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.20 on 2019-04-16 08:24
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('club', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='PayUCardToken',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('disposable_token', models.CharField(max_length=255, unique=True)),
+ ('reusable_token', models.CharField(blank=True, max_length=255, null=True, unique=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='PayUNotification',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('body', models.TextField()),
+ ('received_at', models.DateTimeField(auto_now_add=True)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='PayUOrder',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('pos_id', models.CharField(max_length=255)),
+ ('customer_ip', models.GenericIPAddressField()),
+ ('amount', models.PositiveIntegerField()),
+ ('order_id', models.CharField(blank=True, max_length=255)),
+ ('status', models.CharField(blank=True, choices=[('PENDING', 'Pending'), ('WAITING_FOR_CONFIRMATION', 'Waiting for confirmation'), ('COMPLETED', 'Completed'), ('CANCELED', 'Canceled'), ('REJECTED', 'Rejected')], max_length=128)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.RemoveField(
+ model_name='payment',
+ name='schedule',
+ ),
+ migrations.AddField(
+ model_name='membership',
+ name='honorary',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='plan',
+ name='active',
+ field=models.BooleanField(default=True, verbose_name='active'),
+ ),
+ migrations.AlterField(
+ model_name='schedule',
+ name='method',
+ field=models.CharField(choices=[('payu-re', 'PayU Recurring'), ('paypal-re', 'PayPal Recurring')], max_length=255, verbose_name='method'),
+ ),
+ migrations.DeleteModel(
+ name='Payment',
+ ),
+ migrations.AddField(
+ model_name='payuorder',
+ name='schedule',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='club.Schedule'),
+ ),
+ migrations.AddField(
+ model_name='payunotification',
+ name='order',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='club.PayUOrder'),
+ ),
+ migrations.AddField(
+ model_name='payucardtoken',
+ name='schedule',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='club.Schedule'),
+ ),
+ ]
--- /dev/null
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.20 on 2019-04-16 08:45
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('club', '0002_auto_20190416_1024'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='payuorder',
+ name='amount',
+ ),
+ ]
--- /dev/null
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.20 on 2019-04-16 08:50
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('club', '0003_remove_payuorder_amount'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='payucardtoken',
+ name='pos_id',
+ field=models.CharField(default='', max_length=255),
+ preserve_default=False,
+ ),
+ ]
--- /dev/null
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.20 on 2019-04-16 08:52
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('club', '0004_payucardtoken_pos_id'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='payucardtoken',
+ name='disposable_token',
+ field=models.CharField(max_length=255),
+ ),
+ migrations.AlterField(
+ model_name='payucardtoken',
+ name='reusable_token',
+ field=models.CharField(blank=True, max_length=255, null=True),
+ ),
+ migrations.AlterUniqueTogether(
+ name='payucardtoken',
+ unique_together=set([('pos_id', 'reusable_token'), ('pos_id', 'disposable_token')]),
+ ),
+ ]
--- /dev/null
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.20 on 2019-04-16 10:36
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('club', '0005_auto_20190416_1052'),
+ ]
+
+ operations = [
+ migrations.AlterUniqueTogether(
+ name='payucardtoken',
+ unique_together=set([]),
+ ),
+ ]
--- /dev/null
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.20 on 2019-04-16 14:25
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('club', '0006_auto_20190416_1236'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='schedule',
+ name='is_active',
+ ),
+ migrations.AlterField(
+ model_name='payunotification',
+ name='order',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_set', to='club.PayUOrder'),
+ ),
+ migrations.AlterField(
+ model_name='schedule',
+ name='method',
+ field=models.CharField(choices=[('payu', 'PayU'), ('payu-re', 'PayU Recurring'), ('paypal-re', 'PayPal Recurring')], max_length=255, verbose_name='method'),
+ ),
+ ]
-# -*- coding: utf-8
-from __future__ import unicode_literals
-
-from datetime import timedelta
+from datetime import datetime, timedelta
from django.conf import settings
+from django.contrib.sites.models import Site
from django.urls import reverse
from django.db import models
from django.utils.timezone import now
-from django.utils.translation import ugettext_lazy as _, ungettext
+from django.utils.translation import ugettext_lazy as _, ungettext, ugettext, get_language
from catalogue.utils import get_random_hash
from .payment_methods import methods, method_by_slug
+from .payu import models as payu_models
class Plan(models.Model):
min_amount = models.DecimalField(_('min_amount'), max_digits=10, decimal_places=2)
allow_recurring = models.BooleanField(_('allow recurring'))
allow_one_time = models.BooleanField(_('allow one time'))
+ active = models.BooleanField(_('active'), default=True)
class Meta:
verbose_name = _('plan')
def get_next_installment(self, date):
if self.interval == self.PERPETUAL:
- return None
+ return datetime.max - timedelta(1)
elif self.interval == self.YEAR:
return date.replace(year=date.year + 1)
elif self.interval == self.MONTH:
return date
-
class Schedule(models.Model):
""" Represents someone taking up a plan. """
key = models.CharField(_('key'), max_length=255, unique=True)
plan = models.ForeignKey(Plan, verbose_name=_('plan'), on_delete=models.PROTECT)
amount = models.DecimalField(_('amount'), max_digits=10, decimal_places=2)
method = models.CharField(_('method'), max_length=255, choices=[(method.slug, method.name) for method in methods])
- is_active = models.BooleanField(_('active'), default=False)
is_cancelled = models.BooleanField(_('cancelled'), default=False)
started_at = models.DateTimeField(_('started at'), auto_now_add=True)
expires_at = models.DateTimeField(_('expires_at'), null=True, blank=True)
- # extra info?
class Meta:
verbose_name = _('schedule')
self.key = get_random_hash(self.email)
return super(Schedule, self).save(*args, **kwargs)
+ def initiate_payment(self, request):
+ return self.get_payment_method().initiate(request, self)
+
+ def pay(self, request):
+ return self.get_payment_method().pay(request, self)
+
def get_absolute_url(self):
return reverse('club_schedule', args=[self.key])
return method_by_slug[self.method]
def is_expired(self):
- return self.expires_at is not None and self.expires_at < now()
+ return self.expires_at is not None and self.expires_at <= now()
- def create_payment(self):
- n = now()
- self.expires_at = self.plan.get_next_installment(n)
- self.is_active = True
- self.save()
- self.payment_set.create(payed_at=n)
-
-
-class Payment(models.Model):
- schedule = models.ForeignKey(Schedule, verbose_name=_('schedule'), on_delete=models.PROTECT)
- created_at = models.DateTimeField(_('created at'), auto_now_add=True)
- payed_at = models.DateTimeField(_('payed at'), null=True, blank=True)
-
- class Meta:
- verbose_name = _('payment')
- verbose_name_plural = _('payments')
-
- def __str__(self):
- return "%s %s" % (self.schedule, self.payed_at)
+ def is_active(self):
+ return self.expires_at is not None and self.expires_at > now()
class Membership(models.Model):
""" Represents a user being recognized as a member of the club. """
user = models.OneToOneField(settings.AUTH_USER_MODEL, verbose_name=_('user'), on_delete=models.CASCADE)
created_at = models.DateTimeField(_('created at'), auto_now_add=True)
+ honorary = models.BooleanField(default=False)
class Meta:
verbose_name = _('membership')
return u'tow. ' + str(self.user)
@classmethod
- def is_active_for(self, user):
+ def is_active_for(cls, user):
if user.is_anonymous:
return False
+ try:
+ membership = user.membership
+ except cls.DoesNotExist:
+ return False
+ if membership.honorary:
+ return True
return Schedule.objects.filter(
- models.Q(expires_at=None) | models.Q(expires_at__lt=now()),
+ expires_at__gt=now(),
membership__user=user,
- is_active=True,
).exists()
else:
return ungettext('a day after expiration', '%d days after expiration', n=-self.days_before)
+
+########
+# #
+# PayU #
+# #
+########
+
+class PayUOrder(payu_models.Order):
+ schedule = models.ForeignKey(Schedule, on_delete=models.CASCADE)
+
+ def get_amount(self):
+ return self.schedule.amount
+
+ def get_buyer(self):
+ return {
+ "email": self.schedule.email,
+ "language": get_language(),
+ }
+
+ def get_continue_url(self):
+ return "https://{}{}".format(
+ Site.objects.get_current().domain,
+ self.schedule.get_absolute_url())
+
+ def get_description(self):
+ return ugettext('Towarzystwo Wolnych Lektur')
+
+ def is_recurring(self):
+ return self.schedule.get_payment_method().is_recurring
+
+ def get_card_token(self):
+ return self.schedule.payucardtoken_set.order_by('-created_at').first()
+
+ def get_notify_url(self):
+ return "https://{}{}".format(
+ Site.objects.get_current().domain,
+ reverse('club_payu_notify', args=[self.pk]))
+
+ def status_updated(self):
+ if self.status == 'COMPLETED':
+ new_exp = self.schedule.plan.get_next_installment(now())
+ if self.schedule.expires_at is None or self.schedule.expires_at < new_exp:
+ self.schedule.expires_at = new_exp
+ self.schedule.save()
+
+
+class PayUCardToken(payu_models.CardToken):
+ schedule = models.ForeignKey(Schedule, on_delete=models.CASCADE)
+
+
+class PayUNotification(payu_models.Notification):
+ order = models.ForeignKey(PayUOrder, models.CASCADE, related_name='notification_set')
+from django.conf import settings
from django.urls import reverse
class PaymentMethod(object):
is_recurring = False
- @classmethod
- def get_payment_url(cls, schedule):
+ def initiate(self, request, schedule):
return reverse('club_dummy_payment', args=[schedule.key])
name = 'PayU'
template_name = 'club/payment/payu.html'
- @classmethod
- def get_payment_url(cls, schedule):
- return reverse('club_dummy_payment', args=[schedule.key])
+ def __init__(self, pos_id):
+ self.pos_id = pos_id
+
+ def initiate(self, request, schedule):
+ # Create Order at once.
+ from .models import PayUOrder
+ order = PayUOrder.objects.create(
+ pos_id=self.pos_id,
+ customer_ip=request.META['REMOTE_ADDR'],
+ schedule=schedule,
+ )
+ return order.put()
class PayURe(PaymentMethod):
template_name = 'club/payment/payu-re.html'
is_recurring = True
- @classmethod
- def get_payment_url(cls, schedule):
- return reverse('club_dummy_payment', args=[schedule.key])
+ def __init__(self, pos_id):
+ self.pos_id = pos_id
+ def initiate(self, request, schedule):
+ return reverse('club_payu_rec_payment', args=[schedule.key])
+
+ def pay(self, request, schedule):
+ # Create order, put it and see what happens next.
+ from .models import PayUOrder
+ order = PayUOrder.objects.create(
+ pos_id=self.pos_id,
+ customer_ip=request.META['REMOTE_ADDR'],
+ schedule=schedule,
+ )
+ return order.put()
+
class PayPalRe(PaymentMethod):
slug='paypal-re'
template_name = 'club/payment/paypal-re.html'
is_recurring = True
- @classmethod
- def get_payment_url(cls, schedule):
+ def initiate(self, request, schedule):
return reverse('club_dummy_payment', args=[schedule.key])
-methods = [
- PayU,
- PayURe,
- PayPalRe,
-]
+methods = []
+
+pos = getattr(settings, 'CLUB_PAYU_POS', None)
+if pos:
+ payu_method = PayU(pos)
+ methods.append(payu_method)
+else:
+ payu_method = None
+
+pos= getattr(settings, 'CLUB_PAYU_RECURRING_POS', None)
+if pos:
+ payure_method = PayURe(pos)
+ methods.append(payure_method)
+else:
+ payure_method = None
+
+
+methods.append(PayPalRe())
+
method_by_slug = {
m.slug: m
--- /dev/null
+from django.conf import settings
+from .pos import POS
+
+
+POSS = {
+ k: POS(k, **v)
+ for (k, v) in settings.PAYU_POS.items()
+}
--- /dev/null
+from django import forms
+
+
+class CardTokenForm(forms.Form):
+ token = forms.CharField(widget=forms.HiddenInput)
+
+ def get_queryset(self, view):
+ raise NotImplementedError()
+
+ def save(self, view):
+ self.instance, created = self.get_queryset(view).get_or_create(
+ pos_id=view.get_pos().pos_id,
+ disposable_token=self.cleaned_data['token']
+ )
--- /dev/null
+import json
+from urllib.parse import urlencode
+from urllib.request import HTTPError
+from django.contrib.sites.models import Site
+from django.db import models
+from django.urls import reverse
+from . import POSS
+
+
+class CardToken(models.Model):
+ """ This should be attached to a payment schedule. """
+ pos_id = models.CharField(max_length=255)
+ disposable_token = models.CharField(max_length=255)
+ reusable_token = models.CharField(max_length=255, null=True, blank=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ abstract = True
+
+
+class Order(models.Model):
+ pos_id = models.CharField(max_length=255) # TODO: redundant?
+ customer_ip = models.GenericIPAddressField()
+ order_id = models.CharField(max_length=255, blank=True)
+
+ status = models.CharField(max_length=128, blank=True, choices=[
+ ('PENDING', 'Pending'),
+ ('WAITING_FOR_CONFIRMATION', 'Waiting for confirmation'),
+ ('COMPLETED', 'Completed'),
+ ('CANCELED', 'Canceled'),
+ ('REJECTED', 'Rejected'),
+ ])
+
+ class Meta:
+ abstract = True
+
+ # These need to be provided in a subclass.
+
+ def get_amount(self):
+ raise NotImplementedError()
+
+ def get_buyer(self):
+ raise NotImplementedError()
+
+ def get_continue_url(self):
+ raise NotImplementedError()
+
+ def get_description(self):
+ raise NotImplementedError()
+
+ def get_products(self):
+ """ At least: name, unitPrice, quantity. """
+ return [
+ {
+ 'name': self.get_description(),
+ 'unitPrice': str(int(self.get_amount() * 100)),
+ 'quantity': '1',
+ },
+ ]
+
+ def is_recurring(self):
+ return False
+
+ def get_notify_url(self):
+ raise NotImplementedError
+
+ def status_updated(self):
+ pass
+
+ #
+
+ def get_pos(self):
+ return POSS[self.pos_id]
+
+ def get_representation(self, token=None):
+ rep = {
+ "notifyUrl": self.get_notify_url(),
+ "customerIp": self.customer_ip,
+ "merchantPosId": self.pos_id,
+ "currencyCode": self.get_pos().currency_code,
+ "totalAmount": str(int(self.get_amount() * 100)),
+ "extOrderId": "wolne-lektury-rcz-%d" % self.pk,
+
+ "buyer": self.get_buyer() or {},
+ "continueUrl": self.get_continue_url(),
+ "description": self.get_description(),
+ "products": self.get_products(),
+ }
+ if token:
+ token = self.get_card_token()
+ rep['recurring'] = 'STANDARD' if token.reusable_token else 'FIRST'
+ rep['payMethods'] = {
+ "payMethod": {
+ "type": "CARD_TOKEN",
+ "value": token.reusable_token or token.disposable_token
+ }
+ }
+ return rep
+
+ def put(self):
+ token = self.get_card_token() if self.is_recurring() else None
+ representation = self.get_representation(token)
+ try:
+ response = self.get_pos().request('POST', 'orders', representation)
+ except HTTPError as e:
+ resp = json.loads(e.read().decode('utf-8'))
+ if resp['status']['statusCode'] == 'ERROR_ORDER_NOT_UNIQUE':
+ pass
+ else:
+ raise
+
+ if token:
+ reusable_token = response.get('payMethods', {}).get('payMethod', {}).get('value', None)
+ if reusable_token:
+ token.reusable_token = reusable_token
+ token.save()
+ # else?
+
+ self.order_id = response['orderId']
+ self.save()
+
+
+ return response.get('redirectUri', self.schedule.get_absolute_url())
+
+
+class Notification(models.Model):
+ """ Add `order` FK to real Order model. """
+ body = models.TextField()
+ received_at = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ abstract = True
+
+ def get_status(self):
+ return json.loads(self.body)['order']['status']
+
+ def apply(self):
+ status = self.get_status()
+ if self.order.status not in (status, 'COMPLETED'):
+ self.order.status = status
+ self.order.save()
+ self.order.status_updated()
--- /dev/null
+import requests
+
+
+class POS:
+ ACCESS_TOKEN_URL = '/pl/standard/user/oauth/authorize'
+ API_BASE = '/api/v2_1/'
+
+ def __init__(self, pos_id, client_secret, secondary_key, sandbox=False, currency_code='PLN'):
+ self.pos_id = pos_id
+ self.client_secret = client_secret
+ self.secondary_key = secondary_key
+ self.sandbox = sandbox
+ self.currency_code = currency_code
+
+ def get_api_host(self):
+ if self.sandbox:
+ return 'https://secure.snd.payu.com'
+ else:
+ return 'https://secure.payu.com'
+
+ def get_access_token(self):
+ response = requests.post(
+ self.get_api_host() + self.ACCESS_TOKEN_URL,
+ data={
+ 'grant_type': 'client_credentials',
+ 'client_id': self.pos_id,
+ 'client_secret': self.client_secret
+ },
+ )
+ assert response.status_code == 200
+ data = response.json()
+ assert data['token_type'] == 'bearer'
+ assert data['grant_type'] == 'client_credentials'
+ return data['access_token']
+
+ def request(self, method, url, data):
+ access_token = self.get_access_token()
+
+ full_url = self.get_api_host() + self.API_BASE + url
+ response = requests.request(
+ method, full_url, json=data, headers={
+ 'Authorization': 'Bearer ' + access_token,
+ },
+ allow_redirects=False
+ )
+ return response.json()
+
--- /dev/null
+from django.contrib.staticfiles.testing import StaticLiveServerTestCase
+from selenium.webdriver.firefox.webdriver import WebDriver
+
+
+class SandboxTestCase(StaticLiveServerTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.selenium = WebDriver()
+ cls.selenium.implicitly_wait(10)
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.selenium.quit()
+ super().tearDownClass()
+
+ def test_payment(self):
+ self.selenium.get('%s%s' % (self.live_server_url, '/towarzystwo/'))
+ from time import sleep
+ sleep(10)
--- /dev/null
+{
+ "orderId": "ORDER_ID",
+ "status": {
+ "statusCode": "WARNING_CONTINUE_3DS",
+ "severity": "WARNING"
+ },
+ "redirectUri": "{redirectUri}"
+}
--- /dev/null
+{
+ "orderId": "ORDER_ID",
+ "payMethods": {
+ "payMethod": {
+ "card": {
+ "number": "424242******4242",
+ "expirationMonth": "12",
+ "expirationYear": "2017"
+ },
+ "type": "CARD_TOKEN",
+ "value": "TOKC_KPNZVSLJUNR4DHF5NPVKDPJGMX7"
+ }
+ },
+ "status": {
+ "statusCode": "SUCCESS",
+ "statusDesc": "Request successful"
+ }
+}
--- /dev/null
+# 1. idziemy do płatności, wyświetlamy widget, klikamy w wydżet
+
+# (pierwsza płatność) OrderCreateRequest z tokenem jednorazowym
+# < możliwa odpowiedź: WARNING_CONTINUE_3DS ([status]status_co
+# < token wielorazowy
+# < błąd http
+# < nieczytelna odpowiedź
+
+# OrderCreateRequest z tokenem wielorazowym
+
+
+
+# integration tests:
+
+# (run on payu sandbox)
+# open browser, display widget, input data, run widget, fetch token
+#
--- /dev/null
+from hashlib import md5, sha256
+from django.conf import settings
+from django import http
+from django.shortcuts import get_object_or_404
+from django.utils.decorators import method_decorator
+from django.utils.translation import get_language
+from django.views.decorators.csrf import csrf_exempt
+from django.views.generic import FormView, TemplateView, View
+
+
+class Payment(TemplateView):
+ pass
+
+
+class RecPayment(FormView):
+ """ Set form_class to a CardTokenForm. """
+ template_name = 'payu/rec_payment.html'
+
+ def get_context_data(self, *args, **kwargs):
+ ctx = super().get_context_data(*args, **kwargs)
+
+ schedule = self.get_schedule()
+ pos = self.get_pos()
+
+ widget_args = {
+ 'merchant-pos-id': pos.pos_id,
+ 'shop-name': "SHOW NAME",
+ 'total-amount': str(int(schedule.amount * 100)),
+ 'currency-code': pos.currency_code,
+ 'customer-language': get_language(), # filter to pos.languages
+ 'customer-email': schedule.email,
+ 'store-card': 'true',
+ 'recurring-payment': 'true',
+ }
+ widget_sig = sha256(
+ (
+ "".join(v for (k, v) in sorted(widget_args.items())) +
+ pos.secondary_key
+ ).encode('utf-8')
+ ).hexdigest()
+
+ ctx['widget_args'] = widget_args
+ ctx['widget_sig'] = widget_sig
+ ctx['schedule'] = schedule
+ ctx['pos'] = pos
+ return ctx
+
+ def form_valid(self, form):
+ form.save(self)
+ return super().form_valid(form)
+
+
+
+@method_decorator(csrf_exempt, name='dispatch')
+class NotifyView(View):
+ """ Set `order_model` in subclass. """
+ def post(self, request, pk):
+ order = get_object_or_404(self.order_model, pk=pk)
+
+ try:
+ openpayu = request.META['HTTP_OPENPAYU_SIGNATURE']
+ openpayu = dict(term.split('=') for term in openpayu.split(';'))
+ assert openpayu['algorithm'] == 'MD5'
+ assert openpayu['content'] == 'DOCUMENT'
+ assert openpayu['sender'] == 'checkout'
+ sig = openpayu['signature']
+ except (KeyError, ValueError, AssertionError):
+ return http.HttpResponseBadRequest('bad')
+
+ document = request.body + order.get_pos().secondary_key.encode('latin1')
+ if md5(document).hexdigest() != sig:
+ return http.HttpResponseBadRequest('wrong')
+
+ notification = order.notification_set.create(
+ body=request.body
+ )
+ notification.apply()
+
+ return http.HttpResponse('ok')
-{% extends "base/base.html" %}
+{% extends request.session.from_app|yesno:"base/app.html,base/base.html" %}
{% block titleextra %}Towarzystwo Wolnych Lektur{% endblock %}
<p> {{ schedule.amount }}</p>
<p> {{ schedule.plan.get_interval_display }}</p>
-<form method="POST" action="">
+<!--form method="POST" action="">
{% csrf_token %}
+
{{ form.as_p }}
<button type='submit'>Zapłać</button>
+</form-->
+
+
+{% if request.GET.p == 'inline' %}
+<div id="payu-widget"></div>
+{% else %}
+{% if request.GET.p == 'popup' %}
+<form action="http://exampledomain.com/processOrder.php" method="post">
+ <button id="pay-button">Pay now</button>
</form>
+<script
+ src="https://secure.payu.com/front/widget/js/payu-bootstrap.js"
+ pay-button="#pay-button"
+ merchant-pos-id="145227"
+ shop-name="Nazwa sklepu"
+ total-amount="9.99"
+ currency-code="PLN"
+ customer-language="pl"
+ store-card="true"
+ customer-email="email@exampledomain.com"
+ sig="250f5f53e465777b6fefb04f171a21b598ccceb2899fc9f229604ad529c69532">
+</script>
+
+
+{% else %}
+ <form method="POST" action="">
+ {% csrf_token %}
+
+ {{ form.as_p }}
+ <button type='submit'>Zapłać</button>
+</form>
+{% endif %}
+{% endif %}
</div>
+<script
+ src="https://secure.payu.com/front/widget/js/payu-bootstrap.js"
+ merchant-pos-id="145227"
+ shop-name="TEST"
+ total-amount="12345"
+ currency-code="PLN"
+ customer-language="en"
+ store-card="true"
+ payu-brand="false"
+ success-callback="test"
+ widget-mode="use"
+ customer-email="test@test.com"
+ sig="203ec8c4b9571ce6b4c03058f57264f04d06d00a86da19390d47ba1be4551578"
+</script>
+
{% endblock %}
-{% extends "base/base.html" %}
+{% extends request.session.from_app|yesno:"base/app.html,base/base.html" %}
{% block titleextra %}Towarzystwo Wolnych Lektur{% endblock %}
--- /dev/null
+{% extends request.session.from_app|yesno:"base/app.html,base/base.html" %}
+
+
+{% block titleextra %}Towarzystwo Wolnych Lektur{% endblock %}
+
+
+{% block body %}
+<div class="white-box normal-text">
+
+ <h1>Płatność cykliczna przez PayU</h1>
+
+ <p> {{ schedule.email }}</p>
+ <p> {{ schedule.amount }}</p>
+ <p> {{ schedule.plan.get_interval_display }}</p>
+
+ <form id="theform" method='POST'>
+ {% csrf_token %}
+ {{ form }}
+ </form>
+
+
+
+<script>
+ function yeahwellwhatever(data) {
+ $("#theform #id_token").val(data.value);
+ $("#theform").submit()
+ }
+</script>
+
+<div id="payu-widget"></div>
+<script
+ src="{{ pos.get_api_host }}/front/widget/js/payu-bootstrap.js"
+
+ {% for k, v in widget_args.items %}
+ {{ k }}="{{ v }}"
+ {% endfor %}
+
+ success-callback="yeahwellwhatever"
+ sig="{{ widget_sig }}">
+</script>
+
+</div>
+
+{% endblock %}
urlpatterns = [
url(r'^$', views.ClubView.as_view()),
url(r'^dolacz/$', views.JoinView.as_view(), name='club_join'),
- url(r'^dolacz/app/$', views.AppJoinView.as_view(), name='club_join'),
url(r'^plan/(?P<key>[-a-z0-9]+)/$', views.ScheduleView.as_view(), name='club_schedule'),
url(r'^przylacz/(?P<key>[-a-z0-9]+)/$', views.claim, name='club_claim'),
url(r'^anuluj/(?P<key>[-a-z0-9]+)/$', views.cancel, name='club_cancel'),
-
url(r'^testowa-platnosc/(?P<key>[-a-z0-9]+)/$', views.DummyPaymentView.as_view(), name='club_dummy_payment'),
+
+ url(r'platnosc/payu/cykl/(?P<key>.+)/', views.PayURecPayment.as_view(), name='club_payu_rec_payment'),
+ url(r'platnosc/payu/(?P<key>.+)/', views.PayUPayment.as_view(), name='club_payu_payment'),
+
+ url(r'notify/(?P<pk>\d+)/', views.PayUNotifyView.as_view(), name='club_payu_notify'),
+
]
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseRedirect
-from django.shortcuts import render
-from django.views.generic import FormView, CreateView, TemplateView
+from django.shortcuts import get_object_or_404, render
+from django.views.generic import FormView, CreateView, TemplateView, DetailView
from django.views import View
-from .forms import ScheduleForm
+from .payu import POSS
+from .payu import views as payu_views
+from .forms import ScheduleForm, PayUCardTokenForm
from . import models
from .helpers import get_active_schedule
+from .payment_methods import payure_method
class ClubView(TemplateView):
form_class = ScheduleForm
template_name = 'club/membership_form.html'
+ def is_app(self):
+ return self.request.GET.get('app')
+
def get(self, request):
+ if self.is_app():
+ request.session['from_app'] = True
+ elif request.session and 'from_app' in request.session:
+ del request.session['from_app']
schedule = get_active_schedule(request.user)
if schedule is not None:
return HttpResponseRedirect(schedule.get_absolute_url())
else:
return super(JoinView, self).get(request)
+ def get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+ kwargs['request'] = self.request
+ return kwargs
+
def get_context_data(self, form=None):
c = super(JoinView, self).get_context_data()
c['membership'] = getattr(self.request.user, 'membership', None)
form.instance.save()
return retval
+ def get_success_url(self):
+ return self.object.initiate_payment(self.request)
-class AppJoinView(JoinView):
- template_name = 'club/membership_form_app.html'
+class ScheduleView(DetailView):
+ model = models.Schedule
+ slug_field = slug_url_kwarg = 'key'
+ template_name = 'club/schedule.html'
-class ScheduleView(View):
- def get(self, request, key):
- schedule = models.Schedule.objects.get(key=key)
- if not schedule.is_active:
- return HttpResponseRedirect(schedule.get_payment_method().get_payment_url(schedule))
+ def post(self, request, key):
+ schedule = self.get_object()
+ if not schedule.is_active():
+ return HttpResponseRedirect(schedule.initiate_payment(request))
else:
- return render(
- request,
- 'club/schedule.html',
- {
- 'schedule': schedule,
- }
- )
+ return HttpResponseRedirect(schedule.get_absolute_url())
@login_required
schedule = models.Schedule.objects.get(key=key)
schedule.create_payment()
return HttpResponseRedirect(schedule.get_absolute_url())
+
+
+class PayUPayment(payu_views.Payment):
+ pass
+
+
+class PayURecPayment(payu_views.RecPayment):
+ form_class = PayUCardTokenForm
+
+ def get_schedule(self):
+ return get_object_or_404(models.Schedule, key=self.kwargs['key'])
+
+ def get_pos(self):
+ pos_id = payure_method.pos_id
+ return POSS[pos_id]
+
+ def get_success_url(self):
+ return self.get_schedule().pay(self.request)
+
+
+class PayUNotifyView(payu_views.NotifyView):
+ order_model = models.PayUOrder
+
--- /dev/null
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.20 on 2019-04-03 13:10
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('social', '0007_auto_20190318_1339'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='cite',
+ options={'ordering': ('vip', 'text'), 'verbose_name': 'banner', 'verbose_name_plural': 'banners'},
+ ),
+ ]
PICTURE_PAGE_SIZE = 20
CONTACT_FORMS_MODULE = 'wolnelektury.contact_forms'
+
+PAYU_POS = {
+ '300746': {
+ 'client_secret': '2ee86a66e5d97e3fadc400c9f19b065d',
+ 'secondary_key': 'b6ca15b0d1020e8094d9b5f8d163db54',
+ 'sandbox': True,
+ },
+}
+
+CLUB_PAYU_POS = '300746'
+CLUB_PAYU_RECURRING_POS = '300746'
+