paypal
authorRadek Czajka <rczajka@rczajka.pl>
Tue, 22 Jun 2021 08:30:51 +0000 (10:30 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Tue, 22 Jun 2021 08:30:51 +0000 (10:30 +0200)
19 files changed:
src/club/admin.py
src/club/forms.py
src/club/migrations/0029_schedule_method.py [new file with mode: 0644]
src/club/migrations/0030_populate_method.py [new file with mode: 0644]
src/club/migrations/0031_auto_20210622_0945.py [new file with mode: 0644]
src/club/models.py
src/club/payment_methods.py
src/club/static/club/club.scss
src/club/static/club/paypal2.png [new file with mode: 0644]
src/club/templates/club/payment/payu-re.html
src/club/templates/club/payment_form.html
src/paypal/admin.py [new file with mode: 0644]
src/paypal/migrations/0004_auto_20210622_0945.py [new file with mode: 0644]
src/paypal/models.py
src/paypal/rest.py
src/paypal/urls.py
src/paypal/views.py
src/wolnelektury/static/js/base.js
src/wolnelektury/urls.py

index 4e85d4d..4d65c3c 100644 (file)
@@ -55,9 +55,12 @@ class ExpiredFilter(YesNoFilter):
 
 
 class ScheduleAdmin(admin.ModelAdmin):
-    list_display = ['email', 'started_at', 'payed_at', 'expires_at', 'amount', 'monthly', 'yearly', 'is_cancelled']
+    list_display = [
+        'email', 'started_at', 'payed_at', 'expires_at', 'amount', 'monthly', 'yearly', 'is_cancelled',
+        'method'
+    ]
     search_fields = ['email']
-    list_filter = ['is_cancelled', 'monthly', 'yearly', PayedFilter, ExpiredFilter, 'source']
+    list_filter = ['is_cancelled', 'monthly', 'yearly', 'method', PayedFilter, ExpiredFilter, 'source']
     date_hierarchy = 'started_at'
     raw_id_fields = ['membership']
     inlines = [PayUOrderInline, PayUCardTokenInline]
index 2664fa3..d1bebe8 100644 (file)
@@ -5,7 +5,7 @@ from decimal import Decimal
 from django import forms
 from django.utils.translation import ugettext as _
 from newsletter.forms import NewsletterForm
-from . import models
+from . import models, payment_methods
 from .payu.forms import CardTokenForm
 
 
@@ -14,10 +14,11 @@ class ScheduleForm(forms.ModelForm, NewsletterForm):
 
     class Meta:
         model = models.Schedule
-        fields = ['monthly', 'amount', 'email']
+        fields = ['monthly', 'amount', 'email', 'method']
         widgets = {
             'amount': forms.HiddenInput,
             'monthly': forms.HiddenInput,
+            'method': forms.HiddenInput,
         }
 
     def __init__(self, referer=None, **kwargs):
@@ -35,6 +36,18 @@ class ScheduleForm(forms.ModelForm, NewsletterForm):
             )
         return value
 
+    def clean_method(self):
+        value = self.cleaned_data['method']
+        monthly = self.cleaned_data['monthly']
+        for m in payment_methods.methods:
+            if m.slug == value:
+                if (monthly and m.is_recurring) or (not monthly and m.is_onetime):
+                    return value
+        if monthly:
+            return payment_methods.recurring_payment_method.slug
+        else:
+            return payment_methods.single_payment_method.slug
+    
     def save(self, *args, **kwargs):
         NewsletterForm.save(self, *args, **kwargs)
         self.instance.source = self.referer or ''
diff --git a/src/club/migrations/0029_schedule_method.py b/src/club/migrations/0029_schedule_method.py
new file mode 100644 (file)
index 0000000..2be768b
--- /dev/null
@@ -0,0 +1,19 @@
+# Generated by Django 2.2.19 on 2021-06-18 10:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('club', '0028_directdebit'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='schedule',
+            name='method',
+            field=models.CharField(choices=[('payu-re', 'payu-re'), ('payu', 'payu'), ('paypal', 'paypal')], default='', max_length=32, verbose_name='method'),
+            preserve_default=False,
+        ),
+    ]
diff --git a/src/club/migrations/0030_populate_method.py b/src/club/migrations/0030_populate_method.py
new file mode 100644 (file)
index 0000000..40b2cdd
--- /dev/null
@@ -0,0 +1,23 @@
+# Generated by Django 2.2.19 on 2021-06-18 10:11
+
+from django.db import migrations, models
+
+
+def populate_method(apps, schema_editor):
+    Schedule = apps.get_model('club', 'Schedule')
+    Schedule.objects.filter(method='', monthly=False, yearly=False).update(method='payu')
+    Schedule.objects.filter(
+        models.Q(monthly=True) | models.Q(yearly=True),
+        method=''
+    ).update(method='payu-re')
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('club', '0029_schedule_method'),
+    ]
+
+    operations = [
+        migrations.RunPython(populate_method, migrations.RunPython.noop)
+    ]
diff --git a/src/club/migrations/0031_auto_20210622_0945.py b/src/club/migrations/0031_auto_20210622_0945.py
new file mode 100644 (file)
index 0000000..01e7c6c
--- /dev/null
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.19 on 2021-06-22 07:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('club', '0030_populate_method'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='schedule',
+            name='method',
+            field=models.CharField(choices=[('payu-re', 'PayU recurring'), ('payu', 'PayU'), ('paypal', 'PayPal')], max_length=32, verbose_name='method'),
+        ),
+    ]
index 9a74e27..8905f08 100644 (file)
@@ -16,7 +16,7 @@ from django.utils.translation import ugettext_lazy as _, ungettext, ugettext, ge
 from catalogue.utils import get_random_hash
 from messaging.states import Level
 from reporting.utils import render_to_pdf
-from .payment_methods import recurring_payment_method, single_payment_method
+from .payment_methods import methods
 from .payu import models as payu_models
 from . import utils
 
@@ -49,6 +49,9 @@ class Schedule(models.Model):
     email = models.EmailField(_('email'))
     membership = models.ForeignKey('Membership', verbose_name=_('membership'), null=True, blank=True, on_delete=models.SET_NULL)
     amount = models.DecimalField(_('amount'), max_digits=10, decimal_places=2)
+    method = models.CharField(_('method'), max_length=32, choices=[
+        (m.slug, m.name) for m in methods
+        ])
     monthly = models.BooleanField(_('monthly'), default=True)
     yearly = models.BooleanField(_('yearly'), default=False)
 
@@ -86,7 +89,7 @@ class Schedule(models.Model):
         return reverse('club_thanks', args=[self.key])
 
     def get_payment_method(self):
-        return recurring_payment_method if self.monthly or self.yearly else single_payment_method
+        return [m for m in methods if m.slug == self.method][0]
 
     def is_expired(self):
         return self.expires_at is not None and self.expires_at <= now()
@@ -97,6 +100,21 @@ class Schedule(models.Model):
     def is_recurring(self):
         return self.monthly or self.yearly
 
+    def set_payed(self):
+        since = self.expires_at
+        n = now()
+        if since is None or since < n:
+            since = n
+        new_exp = self.get_next_installment(since)
+        if self.payed_at is None:
+            self.payed_at = n
+        if self.expires_at is None or self.expires_at < new_exp:
+            self.expires_at = new_exp
+            self.save()
+
+        if not self.email_sent:
+            self.send_email()
+    
     def get_next_installment(self, date):
         if self.yearly:
             return utils.add_year(date)
@@ -247,19 +265,8 @@ class PayUOrder(payu_models.Order):
 
     def status_updated(self):
         if self.status == 'COMPLETED':
-            since = self.schedule.expires_at
-            n = now()
-            if since is None or since < n:
-                since = n
-            new_exp = self.schedule.get_next_installment(since)
-            if self.schedule.payed_at is None:
-                self.schedule.payed_at = n
-            if self.schedule.expires_at is None or self.schedule.expires_at < new_exp:
-                self.schedule.expires_at = new_exp
-                self.schedule.save()
-
-            if not self.schedule.email_sent:
-                self.schedule.send_email()
+            self.schedule.set_payed()
+            
 
     @classmethod
     def send_receipt(cls, email, year):
index 1b27bd3..9b73c89 100644 (file)
@@ -3,6 +3,7 @@
 #
 from django.conf import settings
 from django.urls import reverse
+from paypal.rest import agreement_approval_url
 
 
 class PaymentMethod(object):
@@ -16,6 +17,7 @@ class PaymentMethod(object):
 class PayU(PaymentMethod):
     is_onetime = True
     slug = 'payu'
+    name = 'PayU'
     template_name = 'club/payment/payu.html'
 
     def __init__(self, pos_id):
@@ -33,7 +35,8 @@ class PayU(PaymentMethod):
 
 
 class PayURe(PaymentMethod):
-    slug='payu-re'
+    slug = 'payu-re'
+    name = 'PayU recurring'
     template_name = 'club/payment/payu-re.html'
     is_recurring = True
 
@@ -59,23 +62,35 @@ class PayURe(PaymentMethod):
         
 
 class PayPal(PaymentMethod):
-    slug='paypal'
+    slug = 'paypal'
+    name = 'PayPal'
     template_name = 'club/payment/paypal.html'
     is_recurring = True
-    is_onetime = True
+    is_onetime = False
 
     def initiate(self, request, schedule):
-        return reverse('club_dummy_payment', args=[schedule.key])
+        app = request.GET.get('app')
+        return agreement_approval_url(schedule.amount, schedule.key, app=app)
+
 
+methods = []
 
 pos = getattr(settings, 'CLUB_PAYU_RECURRING_POS', None)
 if pos:
     recurring_payment_method = PayURe(pos)
+    methods.append(recurring_payment_method)
 else:
     recurring_payment_method = None
 
 pos = getattr(settings, 'CLUB_PAYU_POS', None)
 if pos:
     single_payment_method = PayU(pos)
+    methods.append(single_payment_method)
 else:
     single_payment_method = None
+
+
+
+methods.append(
+    PayPal()
+)
index 892faf4..6b533e2 100644 (file)
         flex: 1;
     }
 }
+
+
+.methods {
+    .button {
+        border: 1px solid black;
+        border-radius: 10px;
+        padding: 10px;
+        margin-right: 3%;
+
+        &.active {
+            background: #9ACD3240;
+        }
+    }
+}
diff --git a/src/club/static/club/paypal2.png b/src/club/static/club/paypal2.png
new file mode 100644 (file)
index 0000000..33ea162
Binary files /dev/null and b/src/club/static/club/paypal2.png differ
index b71e87a..c3e3ea5 100644 (file)
@@ -1,8 +1,13 @@
 {% load i18n %}
 {% load static %}
-<span style="vertical-align: bottom; padding-right: 5px; font-size: 12px">
+<span style="padding-right: 5px; font-size: 12px">
   {% trans "Safe payments" %}
 </span>
-<img src="{% static 'club/payu/payu.png' %}">
-<img src="{% static 'club/visa-100.png' %}">
-<img src="{% static 'club/mastercard.png' %}">
+<span class="button active" data-method="payu-re">
+  <img src="{% static 'club/payu/payu.png' %}" alt="PayU">
+  <img src="{% static 'club/visa-100.png' %}" alt="Visa" >
+  <img src="{% static 'club/mastercard.png' %}" alt="Mastercard">
+</span>
+<span class="button" data-method="paypal">
+  <img src="{% static 'club/paypal2.png' %}" alt="PayPal">
+</span>
index 2891b98..72d3e25 100644 (file)
   {% for e in  form.amount.errors %}
     <li>{{ e }}</li>
   {% endfor %}
+  {% for e in  form.method.errors %}
+    <li>{{ e }}</li>
+  {% endfor %}
 </ul>
 
 <h3>1. {% trans "Choose your type of support" %}</h3>
 
 {{ form.amount }}
 {{ form.monthly }}
+{{ form.method }}
 <div class="plan-select">
   <span class="button plan-toggle" data-plan="plan-single" data-monthly="False">{% trans "one-time" %}</span>
   <span class="button plan-toggle active" data-plan="plan-monthly" data-monthly="True">{% trans "monthly" %}</span>
@@ -45,7 +49,9 @@
     <span class="button">{% trans "different amount" %}</span>
     <input type="number" min="{{ club.min_amount }}">
   </span>
-  <div class="methods">{% include 'club/payment/payu-re.html' %}</div>
+  <div class="methods">
+    {% include 'club/payment/payu-re.html' %}
+  </div>
 </div>
 
 <h3>3. {% trans "Provide an e-mail address" %}</h3>
diff --git a/src/paypal/admin.py b/src/paypal/admin.py
new file mode 100644 (file)
index 0000000..c8151fa
--- /dev/null
@@ -0,0 +1,7 @@
+from django.contrib import admin
+from . import models
+
+
+admin.site.register(models.BillingAgreement)
+admin.site.register(models.BillingPlan)
+
diff --git a/src/paypal/migrations/0004_auto_20210622_0945.py b/src/paypal/migrations/0004_auto_20210622_0945.py
new file mode 100644 (file)
index 0000000..cc1b0cd
--- /dev/null
@@ -0,0 +1,25 @@
+# Generated by Django 2.2.19 on 2021-06-22 07:45
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('club', '0031_auto_20210622_0945'),
+        ('paypal', '0003_auto_20190729_1450'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='billingagreement',
+            name='user',
+        ),
+        migrations.AddField(
+            model_name='billingagreement',
+            name='schedule',
+            field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.PROTECT, to='club.Schedule'),
+            preserve_default=False,
+        ),
+    ]
index 2ce3635..3fc012b 100644 (file)
@@ -13,7 +13,7 @@ class BillingPlan(models.Model):
 
 class BillingAgreement(models.Model):
     agreement_id = models.CharField(max_length=32)
-    user = models.ForeignKey(User, models.PROTECT)
+    schedule = models.ForeignKey('club.Schedule', models.PROTECT)
     plan = models.ForeignKey(BillingPlan, models.PROTECT)
     active = models.BooleanField(max_length=32)
     token = models.CharField(max_length=32)
index 92752b7..ff8c851 100644 (file)
@@ -18,8 +18,8 @@ class PaypalError(Exception):
     pass
 
 
-def absolute_url(url_name):
-    return "http://%s%s" % (Site.objects.get_current().domain, reverse(url_name))
+def absolute_url(url_name, kwargs=None):
+    return "http://%s%s" % (Site.objects.get_current().domain, reverse(url_name, kwargs=kwargs))
 
 
 def create_plan(amount):
@@ -28,7 +28,7 @@ def create_plan(amount):
         "description": "Cykliczna darowizna na wsparcie Wolnych Lektur",
         "merchant_preferences": {
             "auto_bill_amount": "yes",
-            "return_url": absolute_url('paypal_return'),
+            "return_url": absolute_url('paypal_return', {'key': '-'}),
             "cancel_url": absolute_url('paypal_cancel'),
             # "initial_fail_amount_action": "continue",
             "max_fail_attempts": "3",
@@ -63,7 +63,7 @@ def get_link(links, rel):
             return link.href
 
 
-def create_agreement(amount, app=False):
+def create_agreement(amount, key, app=False):
     try:
         plan = BillingPlan.objects.get(amount=amount)
     except BillingPlan.DoesNotExist:
@@ -84,8 +84,13 @@ def create_agreement(amount, app=False):
     })
     if app:
         billing_agreement['override_merchant_preferences'] = {
-            'return_url': absolute_url('paypal_app_return'),
+            'return_url': absolute_url('paypal_app_return', {'key': key}),
         }
+    else:
+        billing_agreement['override_merchant_preferences'] = {
+            'return_url': absolute_url('paypal_return', {'key': key}),
+        }
+        
 
     response = billing_agreement.create()
     if response:
@@ -94,8 +99,8 @@ def create_agreement(amount, app=False):
         raise PaypalError(billing_agreement.error)
 
 
-def agreement_approval_url(amount, app=False):
-    agreement = create_agreement(amount, app=app)
+def agreement_approval_url(amount, key, app=False):
+    agreement = create_agreement(amount, key, app=app)
     return get_link(agreement.links, 'approval_url')
 
 
index aca8918..7adf644 100644 (file)
@@ -9,7 +9,7 @@ urlpatterns = (
     path('form/', RedirectView.as_view(url='/towarzystwo/dolacz/')),
     path('app-form/', RedirectView.as_view(url='/towarzystwo/dolacz/app/')),
 
-    path('return/', views.paypal_return, name='paypal_return'),
-    path('app-return/', views.paypal_return, kwargs={'app': True}, name='paypal_app_return'),
+    path('return/<key>/', views.paypal_return, name='paypal_return'),
+    path('app-return/<key>/', views.paypal_return, kwargs={'app': True}, name='paypal_app_return'),
     path('cancel/', views.paypal_cancel, name='paypal_cancel'),
 )
index 01d3a5a..b1720f9 100644 (file)
@@ -6,9 +6,10 @@ from decimal import Decimal
 from django.contrib.auth.decorators import login_required
 from django.http import Http404
 from django.http.response import HttpResponseRedirect, HttpResponseForbidden
-from django.shortcuts import render
+from django.shortcuts import get_object_or_404, render
 
 from api.utils import HttpResponseAppRedirect
+from club.models import Schedule
 from paypal.forms import PaypalSubscriptionForm
 from paypal.rest import execute_agreement, check_agreement, agreement_approval_url, PaypalError
 from paypal.models import BillingAgreement, BillingPlan
@@ -32,7 +33,9 @@ def paypal_form(request, app=False):
 
 
 @login_required
-def paypal_return(request, app=False):
+def paypal_return(request, key, app=False):
+    schedule = get_object_or_404(Schedule, key=key)
+    
     token = request.GET.get('token')
     if not token:
         raise Http404
@@ -43,7 +46,9 @@ def paypal_return(request, app=False):
             plan = BillingPlan.objects.get(amount=amount)
             active = check_agreement(resource.id) or False
             BillingAgreement.objects.create(
-                agreement_id=resource.id, user=request.user, plan=plan, active=active, token=token)
+                agreement_id=resource.id, schedule=schedule, plan=plan, active=active, token=token)
+            if active:
+                schedule.set_payed()
     else:
         resource = None
     if app:
@@ -52,7 +57,7 @@ def paypal_return(request, app=False):
         else:
             return HttpResponseAppRedirect('wolnelekturyapp://paypal_return')
     else:
-        return render(request, 'paypal/return.html', {'resource': resource})
+        return HttpResponseRedirect(schedule.get_thanks_url())
 
 
 def paypal_cancel(request):
index 7d311fc..377a969 100644 (file)
             return false;
         });
 
+        $("#id_method").val('payu-re');
+        $(".methods .button").click(function() {
+            $("#id_method").val($(this).attr('data-method'));
+            $(".methods .button").removeClass('active');
+            $(this).addClass("active");
+        });
+
     });
 })(jQuery);
 
index 09b4d3d..492f1dd 100644 (file)
@@ -59,7 +59,7 @@ urlpatterns += [
     path('towarzystwo/', include('club.urls')),
     #path('pomagam/', include('club.urls2')),
     path('pomagam/', RedirectView.as_view(
-        url='/towarzystwo/', permanent=False)),
+        url='/towarzystwo/?pk_campaign=pomagam', permanent=False)),
     
 
     # Admin panel