From 2f331ef41f1d5db23305ec79a02dfbff342ce893 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 5 Oct 2022 16:32:39 +0200 Subject: [PATCH] Moving away from getpaid for now. --- src/club/management/commands/send_receipts.py | 2 +- src/club/models.py | 18 ++--- src/club/payu/models.py | 10 ++- src/funding/__init__.py | 1 + src/funding/admin.py | 8 +- src/funding/forms.py | 7 ++ .../migrations/0007_auto_20221003_1230.py | 22 ++++++ .../migrations/0008_auto_20221003_1235.py | 59 ++++++++++++++ .../migrations/0009_move_from_getpaid.py | 44 +++++++++++ src/funding/models.py | 79 ++++++++++--------- .../templates/funding/includes/fundings.html | 4 +- src/funding/templatetags/funding_tags.py | 4 - src/funding/urls.py | 2 +- src/funding/views.py | 13 ++- src/wolnelektury/settings/contrib.py | 2 - 15 files changed, 206 insertions(+), 69 deletions(-) create mode 100644 src/funding/migrations/0007_auto_20221003_1230.py create mode 100644 src/funding/migrations/0008_auto_20221003_1235.py create mode 100644 src/funding/migrations/0009_move_from_getpaid.py diff --git a/src/club/management/commands/send_receipts.py b/src/club/management/commands/send_receipts.py index 69cc2f98c..acf8bc417 100644 --- a/src/club/management/commands/send_receipts.py +++ b/src/club/management/commands/send_receipts.py @@ -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() diff --git a/src/club/models.py b/src/club/models.py index ba4466b18..b5df04113 100644 --- a/src/club/models.py +++ b/src/club/models.py @@ -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, }) diff --git a/src/club/payu/models.py b/src/club/payu/models.py index 10dd60aaf..937d32aa6 100644 --- a/src/club/payu/models.py +++ b/src/club/payu/models.py @@ -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): diff --git a/src/funding/__init__.py b/src/funding/__init__.py index d17c68d05..444b24de9 100644 --- a/src/funding/__init__.py +++ b/src/funding/__init__.py @@ -11,6 +11,7 @@ class Settings(AppSettings): DEFAULT_AMOUNT = 20 MIN_AMOUNT = 1 DAYS_NEAR = 2 + PAYU_POS = '300746' app_settings = Settings('FUNDING') diff --git a/src/funding/admin.py b/src/funding/admin.py index f2b1020a4..47bc98a02 100644 --- a/src/funding/admin.py +++ b/src/funding/admin.py @@ -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' ] )] diff --git a/src/funding/forms.py b/src/funding/forms.py index f56c08776..22d1a3092 100644 --- a/src/funding/forms.py +++ b/src/funding/forms.py @@ -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 index 000000000..3fe92ccbd --- /dev/null +++ b/src/funding/migrations/0007_auto_20221003_1230.py @@ -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 index 000000000..40affd788 --- /dev/null +++ b/src/funding/migrations/0008_auto_20221003_1235.py @@ -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 index 000000000..2beeb2484 --- /dev/null +++ b/src/funding/migrations/0009_move_from_getpaid.py @@ -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 + ) + ] diff --git a/src/funding/models.py b/src/funding/models.py index 0e3fa425d..8dbc7111a 100644 --- a/src/funding/models.py +++ b/src/funding/models.py @@ -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' - ) diff --git a/src/funding/templates/funding/includes/fundings.html b/src/funding/templates/funding/includes/fundings.html index 4d4902c84..4c480ff94 100644 --- a/src/funding/templates/funding/includes/fundings.html +++ b/src/funding/templates/funding/includes/fundings.html @@ -5,7 +5,7 @@ {% for funding in fundings %} - +
{{ funding.payed_at.date }}{{ funding.completed_at.date }} {% if funding.name %} {{ funding.name }} @@ -23,4 +23,4 @@ {% endfor %}
{% endspaceless %} -{% paginate %} \ No newline at end of file +{% paginate %} diff --git a/src/funding/templatetags/funding_tags.py b/src/funding/templatetags/funding_tags.py index ece7ba2f1..bfd0e5462 100644 --- a/src/funding/templatetags/funding_tags.py +++ b/src/funding/templatetags/funding_tags.py @@ -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() diff --git a/src/funding/urls.py b/src/funding/urls.py index fd9708fdd..9f788298a 100644 --- a/src/funding/urls.py +++ b/src/funding/urls.py @@ -20,5 +20,5 @@ urlpatterns = [ path('wylacz_email/', banner_exempt(views.DisableNotifications.as_view()), name='funding_disable_notifications'), path('przylacz//', banner_exempt(views.claim), name='funding_claim'), - path('getpaid/', include('getpaid.urls')), + path('notify//', views.PayUNotifyView.as_view(), name='funding_payu_notify'), ] diff --git a/src/funding/views.py b/src/funding/views.py index 750f021d7..71373ae26 100644 --- a/src/funding/views.py +++ b/src/funding/views.py @@ -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 + diff --git a/src/wolnelektury/settings/contrib.py b/src/wolnelektury/settings/contrib.py index ce0882192..b0c95c22f 100644 --- a/src/wolnelektury/settings/contrib.py +++ b/src/wolnelektury/settings/contrib.py @@ -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', ) -- 2.20.1