Moving away from getpaid for now.
authorRadek Czajka <rczajka@rczajka.pl>
Wed, 5 Oct 2022 14:32:39 +0000 (16:32 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Wed, 5 Oct 2022 14:32:39 +0000 (16:32 +0200)
15 files changed:
src/club/management/commands/send_receipts.py
src/club/models.py
src/club/payu/models.py
src/funding/__init__.py
src/funding/admin.py
src/funding/forms.py
src/funding/migrations/0007_auto_20221003_1230.py [new file with mode: 0644]
src/funding/migrations/0008_auto_20221003_1235.py [new file with mode: 0644]
src/funding/migrations/0009_move_from_getpaid.py [new file with mode: 0644]
src/funding/models.py
src/funding/templates/funding/includes/fundings.html
src/funding/templatetags/funding_tags.py
src/funding/urls.py
src/funding/views.py
src/wolnelektury/settings/contrib.py

index 69cc2f9..acf8bc4 100644 (file)
@@ -39,7 +39,7 @@ class Command(BaseCommand):
         )
         emails.update(
             Funding.objects.exclude(email='').filter(
-                payed_at__year=year
+                completed_at__year=year
             ).order_by('email').values_list(
                 'email', flat=True
             ).distinct()
index ba4466b..b5df041 100644 (file)
@@ -310,11 +310,6 @@ class PayUOrder(payu_models.Order):
             "language": get_language(),
         }
 
-    def get_continue_url(self):
-        return "https://{}{}".format(
-            Site.objects.get_current().domain,
-            self.schedule.get_thanks_url())
-
     def get_description(self):
         return 'Wolne Lektury'
 
@@ -329,6 +324,9 @@ class PayUOrder(payu_models.Order):
             Site.objects.get_current().domain,
             reverse('club_payu_notify', args=[self.pk]))
 
+    def get_thanks_url(self):
+        return self.schedule.get_thanks_url()
+
     def status_updated(self):
         if self.status == 'COMPLETED':
             self.schedule.set_payed()
@@ -380,8 +378,8 @@ class PayUOrder(payu_models.Order):
         except Contact.DoesNotExist:
             funding = Funding.objects.filter(
                 email=email,
-                payed_at__year=year,
-                notifications=True).order_by('payed_at').first()
+                completed_at__year=year,
+                notifications=True).order_by('completed_at').first()
             if funding is None:
                 print('no notifications')
                 return
@@ -404,11 +402,11 @@ class PayUOrder(payu_models.Order):
 
         fundings = Funding.objects.filter(
             email=email,
-            payed_at__year=year
-        ).order_by('payed_at')
+            completed_at__year=year
+        ).order_by('completed_at')
         for funding in fundings:
             payments.append({
-                'timestamp': funding.payed_at,
+                'timestamp': funding.completed_at,
                 'amount': funding.amount,
             })
 
index 10dd60a..937d32a 100644 (file)
@@ -77,6 +77,9 @@ class Order(models.Model):
     def get_notify_url(self):
         raise NotImplementedError
 
+    def get_thanks_url(self):
+        raise NotImplementedError
+
     def status_updated(self):
         pass
 
@@ -85,6 +88,11 @@ class Order(models.Model):
     def get_pos(self):
         return POSS[self.pos_id]
 
+    def get_continue_url(self):
+        return "https://{}{}".format(
+            Site.objects.get_current().domain,
+            self.get_thanks_url())
+
     def get_representation(self, token=None):
         rep = {
             "notifyUrl": self.get_notify_url(),
@@ -141,7 +149,7 @@ class Order(models.Model):
             self.order_id = response['orderId']
             self.save()
 
-        return response.get('redirectUri', self.schedule.get_thanks_url())
+        return response.get('redirectUri', self.get_thanks_url())
 
 
 class Notification(models.Model):
index d17c68d..444b24d 100644 (file)
@@ -11,6 +11,7 @@ class Settings(AppSettings):
     DEFAULT_AMOUNT = 20
     MIN_AMOUNT = 1
     DAYS_NEAR = 2
+    PAYU_POS = '300746'
 
 
 app_settings = Settings('FUNDING')
index f2b1020..47bc98a 100644 (file)
@@ -35,9 +35,9 @@ class PayedFilter(admin.SimpleListFilter):
 
     def queryset(self, request, queryset):
         if self.value() == 'yes':
-            return queryset.exclude(payed_at=None)
+            return queryset.exclude(completed_at=None)
         elif self.value() == 'no':
-            return queryset.filter(payed_at=None)
+            return queryset.filter(completed_at=None)
 
 
 class PerksFilter(admin.SimpleListFilter):
@@ -59,13 +59,13 @@ class PerksFilter(admin.SimpleListFilter):
 
 class FundingAdmin(admin.ModelAdmin):
     model = Funding
-    list_display = ['payed_at', 'offer', 'amount', 'name', 'email', 'perk_names']
+    list_display = ['created_at', 'completed_at', 'offer', 'amount', 'name', 'email', 'perk_names']
     search_fields = ['name', 'email', 'offer__title', 'offer__author']
     list_filter = [PayedFilter, 'offer', PerksFilter]
     search_fields = ['user']
     actions = [export_as_csv_action(
         fields=[
-            'id', 'offer', 'name', 'email', 'amount', 'payed_at',
+            'id', 'offer', 'name', 'email', 'amount', 'completed_at',
             'notifications', 'notify_key', 'wl_optout_url'
         ]
     )]
index f56c087..22d1a30 100644 (file)
@@ -7,11 +7,15 @@ from django.utils.safestring import mark_safe
 from django.utils.translation import ugettext_lazy as _, ugettext, get_language
 
 from newsletter.forms import NewsletterForm
+from club.payment_methods import PayU
 from .models import Funding
 from .widgets import PerksAmountWidget
 from . import app_settings
 
 
+payment_method = PayU(app_settings.PAYU_POS)
+
+
 class FundingForm(NewsletterForm):
     required_css_class = 'required'
 
@@ -32,6 +36,7 @@ adres e-mail zostanie wykorzystany także w celu przesyłania newslettera Wolnyc
     def __init__(self, request, offer, *args, **kwargs):
         self.offer = offer
         self.user = request.user if request.user.is_authenticated else None
+        self.client_ip = request.META['REMOTE_ADDR']
         super(FundingForm, self).__init__(*args, **kwargs)
         self.fields['amount'].widget.form_instance = self
 
@@ -59,6 +64,8 @@ adres e-mail zostanie wykorzystany także w celu przesyłania newslettera Wolnyc
             amount=self.cleaned_data['amount'],
             language_code=get_language(),
             user=self.user,
+            pos_id=payment_method.pos_id,
+            customer_ip=self.client_ip,
         )
         funding.perks.set(funding.offer.get_perks(funding.amount))
         return funding
diff --git a/src/funding/migrations/0007_auto_20221003_1230.py b/src/funding/migrations/0007_auto_20221003_1230.py
new file mode 100644 (file)
index 0000000..3fe92cc
--- /dev/null
@@ -0,0 +1,22 @@
+# Generated by Django 2.2.28 on 2022-10-03 10:30
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('funding', '0006_funding_user'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='funding',
+            options={'ordering': ['-completed_at', 'pk'], 'verbose_name': 'funding', 'verbose_name_plural': 'fundings'},
+        ),
+        migrations.RenameField(
+            model_name='funding',
+            old_name='payed_at',
+            new_name='completed_at',
+        ),
+    ]
diff --git a/src/funding/migrations/0008_auto_20221003_1235.py b/src/funding/migrations/0008_auto_20221003_1235.py
new file mode 100644 (file)
index 0000000..40affd7
--- /dev/null
@@ -0,0 +1,59 @@
+# Generated by Django 2.2.28 on 2022-10-03 10:35
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('funding', '0007_auto_20221003_1230'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='funding',
+            name='created_at',
+            field=models.DateTimeField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='funding',
+            name='customer_ip',
+            field=models.GenericIPAddressField(null=True, verbose_name='customer IP'),
+        ),
+        migrations.AddField(
+            model_name='funding',
+            name='order_id',
+            field=models.CharField(blank=True, max_length=255, verbose_name='order ID'),
+        ),
+        migrations.AddField(
+            model_name='funding',
+            name='pos_id',
+            field=models.CharField(default='', max_length=255, verbose_name='POS id'),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='funding',
+            name='status',
+            field=models.CharField(blank=True, choices=[('PENDING', 'Pending'), ('WAITING_FOR_CONFIRMATION', 'Waiting for confirmation'), ('COMPLETED', 'Completed'), ('CANCELED', 'Canceled'), ('REJECTED', 'Rejected'), ('ERR-INVALID_TOKEN', 'Invalid token')], max_length=128),
+        ),
+        migrations.AlterField(
+            model_name='funding',
+            name='completed_at',
+            field=models.DateTimeField(blank=True, null=True),
+        ),
+        migrations.CreateModel(
+            name='PayUNotification',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('body', models.TextField(verbose_name='body')),
+                ('received_at', models.DateTimeField(auto_now_add=True, verbose_name='received_at')),
+                ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_set', to='funding.Funding')),
+            ],
+            options={
+                'verbose_name': 'PayU notification',
+                'verbose_name_plural': 'PayU notifications',
+                'abstract': False,
+            },
+        ),
+    ]
diff --git a/src/funding/migrations/0009_move_from_getpaid.py b/src/funding/migrations/0009_move_from_getpaid.py
new file mode 100644 (file)
index 0000000..2beeb24
--- /dev/null
@@ -0,0 +1,44 @@
+# Generated by Django 2.2.28 on 2022-10-03 10:55
+from django.conf import settings
+from django.db import migrations
+
+
+def move_from_getpaid(apps, schema_editor):
+    try:
+        G = settings.GETPAID_BACKENDS_SETTINGS
+    except AttributeError:
+        G = {}
+        getpaid_conf = False
+    else:
+        getpaid_conf = True
+
+    Funding = apps.get_model('funding', 'Funding')
+    for f in Funding.objects.filter(status=''):
+        payment = f.payment.first()
+        # TODO: what happens when no payments any more?
+        if payment is None:
+            continue
+        f.created_at = payment.created_on
+        f.order_id = payment.external_id
+        f.pos_id = G.get(payment.backend, {}).get('pos_id', '')
+        assert getpaid_conf, 'Getpaid configuration removed prematurely.'
+        f.status = {
+            'paid': 'COMPLETED',
+            'failed': 'REJECTED',
+            'in_progress': 'CANCELLED',
+        }[payment.status]
+        f.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('funding', '0008_auto_20221003_1235'),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            move_from_getpaid,
+            migrations.RunPython.noop
+        )
+    ]
index 0e3fa42..8dbc711 100644 (file)
@@ -13,12 +13,13 @@ from django.urls import reverse
 from django.utils.html import mark_safe
 from django.utils.timezone import utc
 from django.utils.translation import ugettext_lazy as _, override
-import getpaid
 from catalogue.models import Book
 from catalogue.utils import get_random_hash
 from polls.models import Poll
+import club.payu.models
 from wolnelektury.utils import cached_render, clear_cached_renders
 from . import app_settings
+from . import utils
 
 
 class Offer(models.Model):
@@ -63,6 +64,9 @@ class Offer(models.Model):
             self.notify_published()
         return retval
 
+    def get_payu_payment_title(self):
+        return utils.sanitize_payment_title(str(self))
+
     def clear_cache(self):
         clear_cached_renders(self.top_bar)
         clear_cached_renders(self.list_bar)
@@ -134,7 +138,7 @@ class Offer(models.Model):
         return Funding.payed().filter(offer=self)
 
     def funders(self):
-        return self.funding_payed().order_by('-amount', 'payed_at')
+        return self.funding_payed().order_by('-amount', 'completed_at')
 
     def sum(self):
         """ The money gathered. """
@@ -261,18 +265,19 @@ class Perk(models.Model):
         return "%s (%s%s)" % (self.name, self.price, " for %s" % self.offer if self.offer else "")
 
 
-class Funding(models.Model):
+class Funding(club.payu.models.Order):
     """ A person paying in a fundraiser.
 
-    The payment was completed if and only if payed_at is set.
+    The payment was completed if and only if completed_at is set.
 
     """
     offer = models.ForeignKey(Offer, models.PROTECT, verbose_name=_('offer'))
+    customer_ip = models.GenericIPAddressField(_('customer IP'), null=True)
+    
     name = models.CharField(_('name'), max_length=127, blank=True)
     email = models.EmailField(_('email'), blank=True, db_index=True)
     user = models.ForeignKey(settings.AUTH_USER_MODEL, models.SET_NULL, blank=True, null=True)
     amount = models.DecimalField(_('amount'), decimal_places=2, max_digits=10)
-    payed_at = models.DateTimeField(_('payed at'), null=True, blank=True, db_index=True)
     perks = models.ManyToManyField(Perk, verbose_name=_('perks'), blank=True)
     language_code = models.CharField(max_length=2, null=True, blank=True)
     notifications = models.BooleanField(_('notifications'), default=True, db_index=True)
@@ -281,18 +286,43 @@ class Funding(models.Model):
     class Meta:
         verbose_name = _('funding')
         verbose_name_plural = _('fundings')
-        ordering = ['-payed_at', 'pk']
+        ordering = ['-completed_at', 'pk']
 
     @classmethod
     def payed(cls):
         """ QuerySet for all completed payments. """
-        return cls.objects.exclude(payed_at=None)
+        return cls.objects.exclude(completed_at=None)
 
     def __str__(self):
         return str(self.offer)
 
-    def get_absolute_url(self):
-        return reverse('funding_funding', args=[self.pk])
+    def get_amount(self):
+        return self.amount
+
+    def get_buyer(self):
+        return {
+            "email": self.email,
+            "language": self.language_code,
+        }
+
+    def get_description(self):
+        return self.offer.get_payu_payment_title()
+
+    def get_thanks_url(self):
+        return reverse('funding_thanks')
+
+    def status_updated(self):
+        if self.status == 'COMPLETED':
+            if self.email:
+                self.notify(
+                    _('Thank you for your support!'),
+                    'funding/email/thanks.txt'
+                )
+
+    def get_notify_url(self):
+        return "https://{}{}".format(
+            Site.objects.get_current().domain,
+            reverse('funding_payu_notify', args=[self.pk]))
 
     def perk_names(self):
         return ", ".join(perk.name for perk in self.perks.all())
@@ -319,7 +349,7 @@ class Funding(models.Model):
     def notify_funders(cls, subject, template_name, extra_context=None, query_filter=None, payed_only=True):
         funders = cls.objects.exclude(email="").filter(notifications=True)
         if payed_only:
-            funders = funders.exclude(payed_at=None)
+            funders = funders.exclude(completed_at=None)
         if query_filter is not None:
             funders = funders.filter(query_filter)
         emails = set()
@@ -346,8 +376,8 @@ class Funding(models.Model):
         type(self).objects.filter(email=self.email).update(notifications=False)
 
 
-# Register the Funding model with django-getpaid for payments.
-getpaid.register_to_payment(Funding, unique=False, related_name='payment')
+class PayUNotification(club.payu.models.Notification):
+    order = models.ForeignKey(Funding, models.CASCADE, related_name='notification_set')
 
 
 class Spent(models.Model):
@@ -364,28 +394,3 @@ class Spent(models.Model):
     def __str__(self):
         return "Spent: %s" % str(self.book)
 
-
-@receiver(getpaid.signals.new_payment_query)
-def new_payment_query_listener(sender, order=None, payment=None, **kwargs):
-    """ Set payment details for getpaid. """
-    payment.amount = order.amount
-    payment.currency = 'PLN'
-
-
-@receiver(getpaid.signals.user_data_query)
-def user_data_query_listener(sender, order, user_data, **kwargs):
-    """ Set user data for payment. """
-    user_data['email'] = order.email
-
-
-@receiver(getpaid.signals.payment_status_changed)
-def payment_status_changed_listener(sender, instance, old_status, new_status, **kwargs):
-    """ React to status changes from getpaid. """
-    if old_status != 'paid' and new_status == 'paid':
-        instance.order.payed_at = datetime.utcnow().replace(tzinfo=utc)
-        instance.order.save()
-        if instance.order.email:
-            instance.order.notify(
-                _('Thank you for your support!'),
-                'funding/email/thanks.txt'
-            )
index 4d4902c..4c480ff 100644 (file)
@@ -5,7 +5,7 @@
   <table class="wlfund">
     {% for funding in fundings %}
       <tr class="funding-plus">
-        <td class="oneline">{{ funding.payed_at.date }}</td>
+        <td class="oneline">{{ funding.completed_at.date }}</td>
         <td>
           {% if funding.name %}
             {{ funding.name }}
@@ -23,4 +23,4 @@
     {% endfor %}
   </table>
 {% endspaceless %}
-{% paginate %}
\ No newline at end of file
+{% paginate %}
index ece7ba2..bfd0e54 100644 (file)
@@ -6,7 +6,6 @@ from django.template.loader import render_to_string
 from django.core.paginator import Paginator, InvalidPage
 
 from ..models import Offer
-from ..utils import sanitize_payment_title
 
 
 register = template.Library()
@@ -18,9 +17,6 @@ def funding_top_bar():
     return offer.top_bar() if offer is not None else ''
 
 
-register.filter(sanitize_payment_title)
-
-
 @register.simple_tag(takes_context=True)
 def fundings(context, offer):
     fundings = offer.funding_payed()
index fd9708f..9f78829 100644 (file)
@@ -20,5 +20,5 @@ urlpatterns = [
     path('wylacz_email/', banner_exempt(views.DisableNotifications.as_view()), name='funding_disable_notifications'),
     path('przylacz/<key>/', banner_exempt(views.claim), name='funding_claim'),
 
-    path('getpaid/', include('getpaid.urls')),
+    path('notify/<int:pk>/', views.PayUNotifyView.as_view(), name='funding_payu_notify'),
 ]
index 750f021..71373ae 100644 (file)
@@ -7,7 +7,7 @@ from django.urls import reverse
 from django.contrib.auth.decorators import login_required
 from django.views.decorators.csrf import csrf_exempt
 from django.views.generic import TemplateView, FormView, ListView
-from getpaid.models import Payment
+import club.payu.views
 from . import app_settings
 from .forms import FundingForm
 from .models import Offer, Spent, Funding
@@ -72,7 +72,6 @@ class WLFundView(TemplateView):
 class OfferDetailView(FormView):
     form_class = FundingForm
     template_name = "funding/offer_detail.html"
-    backend = 'getpaid.backends.payu'
 
     @csrf_exempt
     def dispatch(self, request, slug=None):
@@ -106,11 +105,7 @@ class OfferDetailView(FormView):
 
     def form_valid(self, form):
         funding = form.save()
-        # Skip getpaid.forms.PaymentMethodForm, go directly to the broker.
-        payment = Payment.create(funding, self.backend)
-        gateway_url_tuple = payment.get_processor()(payment).get_gateway_url(self.request)
-        payment.change_status('in_progress')
-        return redirect(gateway_url_tuple[0])
+        return redirect(funding.put())
 
 
 class CurrentView(OfferDetailView):
@@ -171,3 +166,7 @@ def claim(request, key):
         else funding.offer.get_absolute_url()
     )
 
+
+class PayUNotifyView(club.payu.views.NotifyView):
+    order_model = Funding
+
index ce08821..b0c95c2 100644 (file)
@@ -12,8 +12,6 @@ MIGRATION_MODULES = {
     'getpaid': 'wolnelektury.migrations.getpaid',
 }
 
-GETPAID_ORDER_DESCRIPTION = "{% load funding_tags %}{{ order|sanitize_payment_title }}"
-
 GETPAID_BACKENDS = (
     'getpaid.backends.payu',
 )