Commit 5c09f224 authored by Sébastiaan Versteeg's avatar Sébastiaan Versteeg Committed by Job Doesburg
Browse files

Add create_payment helper function

parent aec97de8
......@@ -40,6 +40,14 @@ payments.apps module
:undoc-members:
:show-inheritance:
payments.exceptions module
--------------------------
.. automodule:: payments.exceptions
:members:
:undoc-members:
:show-inheritance:
payments.forms module
---------------------
......
......@@ -9,10 +9,12 @@ from django.urls import reverse
from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.text import format_lazy
from django.template.defaulttags import date
from django.utils.translation import gettext_lazy as _
from tinymce.models import HTMLField
from members.models import Member
from payments.models import Payable
from pushnotifications.models import ScheduledMessage, Category
from utils.translation import ModelTranslateMeta, MultilingualField
from announcements.models import Slide
......@@ -513,7 +515,7 @@ def registration_member_choices_limit():
)
class Registration(models.Model):
class Registration(models.Model, Payable):
"""Describes a registration for an Event"""
event = models.ForeignKey(Event, models.CASCADE)
......@@ -636,6 +638,24 @@ class Registration(models.Model):
else:
return "{}: {}".format(self.name, self.event)
@property
def payment_amount(self):
return self.event.price
@property
def payment_topic(self):
return f'{self.event.title_en} [{date(self.event.start, "Y-m-d")}]'
@property
def payment_notes(self):
notes = f"Event registration {self.event.title_en}. "
notes += f"{self.event.start}. " f"Registration date: {self.date}."
return notes
@property
def payment_payer(self):
return self.member
class Meta:
ordering = ("date",)
unique_together = (("member", "event"),)
......
......@@ -7,7 +7,9 @@ from django.utils.translation import gettext_lazy as _, get_language
from events import emails
from events.exceptions import RegistrationError
from events.models import Registration, RegistrationInformationField, Event
from payments.exceptions import PaymentError
from payments.models import Payment
from payments.services import create_payment, delete_payment
from utils.snippets import datetime_to_lectureyear
......@@ -160,37 +162,18 @@ def pay_with_tpay(member, event):
:param member: the user
:param event: the event
"""
registration = None
try:
registration = Registration.objects.get(event=event, member=member)
except Registration.DoesNotExist:
raise RegistrationError(_("You are not registered for this event."))
if member.tpay_enabled:
if registration.payment is None:
note = f"Event registration {registration.event.title_en}. "
if registration.name:
note += f"Paid by {registration.name}. "
note += (
f"{registration.event.start}. "
f"Registration date: {registration.date}."
)
registration.payment = Payment.objects.create(
amount=registration.event.price,
paid_by=member,
notes=note,
processed_by=member,
type=Payment.TPAY,
)
registration.save()
elif registration.payment.type == Payment.NONE:
registration.payment.type = Payment.TPAY
registration.save()
else:
raise RegistrationError(_("You have already paid for this " "event."))
if registration.payment is None:
registration.payment = create_payment(
payable=registration, processed_by=member, pay_type=Payment.TPAY
)
registration.save()
else:
raise RegistrationError(_("You do not have Thalia Pay enabled."))
raise RegistrationError(_("You have already paid for this event."))
def update_registration(
......@@ -311,41 +294,13 @@ def update_registration_by_organiser(registration, member, data):
if "payment" in data:
if data["payment"]["type"] == Payment.NONE and registration.payment is not None:
p = registration.payment
registration.payment = None
registration.save()
p.delete()
elif (
data["payment"]["type"] != Payment.NONE and registration.payment is not None
):
if data["payment"]["type"] != Payment.TPAY or (
data["payment"]["type"] == Payment.TPAY and member.tpay_enabled
):
registration.payment.type = data["payment"]["type"]
registration.payment.save()
else:
raise RegistrationError(_("This user does not have Thalia Pay enabled"))
elif data["payment"]["type"] != Payment.NONE and registration.payment is None:
if data["payment"]["type"] != Payment.TPAY or (
data["payment"]["type"] == Payment.TPAY and member.tpay_enabled
):
note = f"Event registration {registration.event.title_en}. "
if registration.name:
note += f"Paid by {registration.name}. "
note += (
f"{registration.event.start}. "
f"Registration date: {registration.date}."
)
registration.payment = Payment.objects.create(
amount=registration.event.price,
paid_by=registration.member,
notes=note,
processed_by=member,
type=data["payment"]["type"],
)
else:
raise RegistrationError(_("This user does not have Thalia Pay enabled"))
delete_payment(registration)
else:
registration.payment = create_payment(
payable=registration,
processed_by=member,
pay_type=data["payment"]["type"],
)
if "present" in data:
registration.present = data["present"]
......
......@@ -44,7 +44,7 @@ class PaymentAdmin(admin.ModelAdmin):
"type",
"paid_by_link",
"processed_by_link",
"notes",
"topic",
)
list_filter = ("type",)
list_select_related = (
......@@ -59,6 +59,7 @@ class PaymentAdmin(admin.ModelAdmin):
"processing_date",
"paid_by",
"processed_by",
"topic",
"notes",
)
readonly_fields = (
......@@ -68,9 +69,11 @@ class PaymentAdmin(admin.ModelAdmin):
"processing_date",
"paid_by",
"processed_by",
"topic",
"notes",
)
search_fields = (
"topic",
"notes",
"paid_by__username",
"paid_by__first_name",
......
class PaymentError(Exception):
"""Custom error for problems during payment"""
pass
# Generated by Django 3.0.2 on 2020-02-21 16:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('payments', '0005_auto_20191030_2055'),
]
operations = [
migrations.AddField(
model_name='payment',
name='topic',
field=models.CharField(default='Unknown', max_length=255),
),
]
......@@ -11,24 +11,6 @@ from localflavor.generic.countries.sepa import IBAN_SEPA_COUNTRIES
from localflavor.generic.models import IBANField, BICField
class Payable:
@property
def payment_amount(self):
raise NotImplementedError
@property
def payment_topic(self):
raise NotImplementedError
@property
def payment_notes(self):
raise NotImplementedError
@property
def payment_payer(self):
raise NotImplementedError
class Payment(models.Model):
"""
Describes a payment
......@@ -79,6 +61,7 @@ class Payment(models.Model):
)
notes = models.TextField(blank=True, null=True)
topic = models.CharField(max_length=255, default="Unknown")
@property
def processed(self):
......@@ -215,3 +198,26 @@ class BankAccount(models.Model):
class Meta:
ordering = ("created_at",)
class Payable:
payment = None
@property
def payment_amount(self):
raise NotImplementedError
@property
def payment_topic(self):
raise NotImplementedError
@property
def payment_notes(self):
raise NotImplementedError
@property
def payment_payer(self):
raise NotImplementedError
def save(self):
raise NotImplementedError
"""The services defined by the payments package"""
import datetime
from typing import Union
from django.db.models import QuerySet, Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from members.models import Member
from .models import Payment, BankAccount
from .exceptions import PaymentError
from .models import Payment, BankAccount, Payable
def create_payment(
payable: Payable,
processed_by: Member,
pay_type: Union[Payment.CASH, Payment.CARD, Payment.WIRE, Payment.TPAY],
) -> Payment:
"""
Create a new payment from a payable object
:param payable: Payable object
:param processed_by: Member that processed this payment
:param pay_type: Payment type
:return: Payment object
"""
if pay_type == Payment.TPAY and not payable.payment_payer.tpay_enabled:
raise PaymentError(_("This user does not have Thalia Pay enabled"))
if payable.payment is not None:
payable.payment.type = pay_type
payable.payment.save()
else:
payable.payment = Payment.objects.create(
processed_by=processed_by,
amount=payable.payment_amount,
notes=payable.payment_notes,
topic=payable.payment_topic,
paid_by=payable.payment_payer,
processing_date=timezone.now(),
type=pay_type,
)
return payable.payment
def delete_payment(payable: Payable):
"""
Removes a payment from a payable object
:param payable: Payable object
:return:
"""
payment = payable.payment
payable.payment = None
payable.save()
payment.delete()
def process_payment(
......
......@@ -30,6 +30,11 @@ class PayableTest(TestCase):
with self.assertRaises(NotImplementedError):
_ = p.payment_payer
def test_save_not_implemented(self):
p = Payable()
with self.assertRaises(NotImplementedError):
p.save()
@override_settings(SUSPEND_SIGNALS=True)
class PaymentTest(TestCase):
......
from unittest.mock import MagicMock
from django.test import TestCase, override_settings
from django.utils import timezone
from freezegun import freeze_time
from members.models import Member
from payments import services
from payments.models import BankAccount, Payment
from payments.exceptions import PaymentError
from payments.models import BankAccount, Payment, Payable
class MockPayable(Payable):
save = MagicMock()
def __init__(
self, payer, amount=5, topic="mock topic", notes="mock notes", payment=None
) -> None:
super().__init__()
self.payer = payer
self.amount = amount
self.topic = topic
self.notes = notes
self.payment = payment
@property
def payment_amount(self):
return self.amount
@property
def payment_topic(self):
return self.topic
@property
def payment_notes(self):
return self.notes
@property
def payment_payer(self):
return self.payer
@freeze_time("2019-01-01")
......@@ -20,6 +53,41 @@ class ServicesTest(TestCase):
def setUpTestData(cls):
cls.member = Member.objects.filter(last_name="Wiggers").first()
def test_create_payment(self):
with self.subTest("Creates new payment with right payment type"):
p = services.create_payment(
MockPayable(self.member), self.member, Payment.CASH
)
self.assertEqual(p.processing_date, timezone.now())
self.assertEqual(p.amount, 5)
self.assertEqual(p.topic, "mock topic")
self.assertEqual(p.notes, "mock notes")
self.assertEqual(p.paid_by, self.member)
self.assertEqual(p.processed_by, self.member)
self.assertEqual(p.type, Payment.CASH)
with self.subTest("Does not create new payment if one already exists"):
existing_payment = Payment(amount=2)
p = services.create_payment(
MockPayable(payer=self.member, payment=existing_payment),
self.member,
Payment.CASH,
)
self.assertEqual(p, existing_payment)
self.assertEqual(p.amount, 2)
with self.subTest("Does not allow Thalia Pay when not enabled"):
with self.assertRaises(PaymentError):
services.create_payment(
MockPayable(payer=self.member), self.member, Payment.TPAY
)
def test_delete_payment(self):
existing_payment = MagicMock()
payable = MockPayable(payer=self.member, payment=existing_payment)
services.delete_payment(payable)
self.assertIsNone(payable.payment)
payable.save.assert_called_once()
existing_payment.delete.assert_called_once()
def test_process_payment(self):
BankAccount.objects.create(
owner=self.member,
......
......@@ -4,6 +4,7 @@ from django.db import models
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.template.defaulttags import date
from events.models import Event
import members
......@@ -223,7 +224,11 @@ class Order(models.Model):
self.payment.save()
except ObjectDoesNotExist:
self.payment = Payment.objects.create(
amount=self.product.price, notes=notes, paid_by=self.member
amount=self.product.price,
notes=notes,
paid_by=self.member,
topic=f"Pizzas {self.pizza_event.event.title_en} "
f'[{date(self.pizza_event.start, "Y-m-d")}]',
)
super().save(*args, **kwargs)
......
......@@ -233,11 +233,13 @@ def _create_payment_for_entry(entry: Entry) -> Payment:
if entry.contribution and entry.membership_type == Membership.BENEFACTOR:
amount = entry.contribution
notes = f"Membership registration. {entry.get_membership_type_display()}."
topic = f"Member registration [{entry.membership_type.upper()}]"
try:
renewal = entry.renewal
membership = renewal.member.latest_membership
notes = f"Membership renewal. {entry.get_membership_type_display()}."
topic = f"Member renewal [{entry.membership_type.upper()}]"
# Having a latest membership which has an until date implies that this
# membership lasts/lasted till the end of the lecture year
# This means it's possible to renew the 'year' membership
......@@ -262,7 +264,7 @@ def _create_payment_for_entry(entry: Entry) -> Payment:
except Renewal.DoesNotExist:
pass
return Payment.objects.create(amount=amount, notes=notes)
return Payment.objects.create(amount=amount, notes=notes, topic=topic)
def _create_member_from_registration(registration: Registration) -> Member:
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment