We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Unprocessed Thalia Pay payment without bank account
Apparently it is possible that people either pay with Thalia pay without having a valid bank account, or, it is possible to remove a bank account after a Thalia Pay payment is made but not processed (in which case it should not be possible)


Sentry Issue: [CONCREXIT-HD](https://sentry.io/organizations/thalia/issues/3640470247/?referrer=github_integration)

```
AttributeError: 'NoneType' object has no attribute 'last_used'
(8 additional frame(s) were not displayed)
...
  File "django/utils/decorators.py", line 46, in _wrapper
    return bound_method(*args, **kwargs)
  File "django/contrib/auth/decorators.py", line 23, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "django/views/generic/base.py", line 119, in dispatch
    return handler(request, *args, **kwargs)
  File "payments/admin_views.py", line 107, in post
    services.process_batch(batch)
  File "payments/services.py", line 151, in process_batch
    bank_account.last_used = batch.withdrawal_date
```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `website/payments/services.py`
Content:
```
1 """The services defined by the payments package."""
2 import datetime
3 from typing import Union
4 
5 from django.conf import settings
6 from django.db.models import Model, Q, QuerySet, Sum
7 from django.urls import reverse
8 from django.utils import timezone
9 from django.utils.translation import gettext_lazy as _
10 
11 from members.models import Member
12 from utils.snippets import send_email
13 
14 from .exceptions import PaymentError
15 from .models import BankAccount, Payment, PaymentUser
16 from .payables import Payable, payables
17 
18 
19 def create_payment(
20     model_payable: Union[Model, Payable],
21     processed_by: Member,
22     pay_type: Union[Payment.CASH, Payment.CARD, Payment.WIRE, Payment.TPAY],
23 ) -> Payment:
24     """Create a new payment from a payable object.
25 
26     :param model_payable: Payable or Model object
27     :param processed_by: PaymentUser that processed this payment
28     :param pay_type: Payment type
29     :return: Payment object
30     """
31     if pay_type not in (Payment.CASH, Payment.CARD, Payment.WIRE, Payment.TPAY):
32         raise PaymentError("Invalid payment type")
33 
34     if isinstance(model_payable, Payable):
35         payable = model_payable
36     else:
37         payable = payables.get_payable(model_payable)
38 
39     payer = (
40         PaymentUser.objects.get(pk=payable.payment_payer.pk)
41         if payable.payment_payer
42         else None
43     )
44 
45     if not (
46         (payer and payer == processed_by and pay_type == Payment.TPAY)
47         or (payable.can_manage_payment(processed_by) and pay_type != Payment.TPAY)
48     ):
49         raise PaymentError(
50             _("User processing payment does not have the right permissions")
51         )
52 
53     if payable.payment_amount == 0:
54         raise PaymentError(_("Payment amount 0 is not accepted"))
55 
56     if pay_type == Payment.TPAY and not payer.tpay_enabled:
57         raise PaymentError(_("This user does not have Thalia Pay enabled"))
58 
59     if not payable.paying_allowed:
60         raise PaymentError(_("Payment restricted"))
61 
62     if payable.payment is not None:
63         payable.payment.amount = payable.payment_amount
64         payable.payment.notes = payable.payment_notes
65         payable.payment.topic = payable.payment_topic
66         payable.payment.paid_by = payer
67         payable.payment.processed_by = processed_by
68         payable.payment.type = pay_type
69         payable.payment.save()
70     else:
71         payable.payment = Payment.objects.create(
72             processed_by=processed_by,
73             amount=payable.payment_amount,
74             notes=payable.payment_notes,
75             topic=payable.payment_topic,
76             paid_by=payer,
77             type=pay_type,
78         )
79     return payable.payment
80 
81 
82 def delete_payment(model: Model, member: Member = None, ignore_change_window=False):
83     """Remove a payment from a payable object.
84 
85     :param model: Payable or Model object
86     :param member: member deleting the payment
87     :param ignore_change_window: ignore the payment change window
88     :return:
89     """
90     payable = payables.get_payable(model)
91 
92     if member and not payable.can_manage_payment(member):
93         raise PaymentError(
94             _("User deleting payment does not have the right permissions.")
95         )
96 
97     payment = payable.payment
98     if (
99         payment.created_at
100         < timezone.now() - timezone.timedelta(seconds=settings.PAYMENT_CHANGE_WINDOW)
101         and not ignore_change_window
102     ):
103         raise PaymentError(_("This payment cannot be deleted anymore."))
104     if payment.batch and payment.batch.processed:
105         raise PaymentError(
106             _("This payment has already been processed and hence cannot be deleted.")
107         )
108 
109     payable.payment = None
110     payable.model.save()
111     payment.delete()
112 
113 
114 def update_last_used(queryset: QuerySet, date: datetime.date = None) -> int:
115     """Update the last used field of a BankAccount queryset.
116 
117     :param queryset: Queryset of BankAccounts
118     :param date: date to set last_used to
119     :return: number of affected rows
120     """
121     if not date:
122         date = timezone.now().date()
123 
124     result = queryset.filter(
125         (Q(valid_from__gte=timezone.now()) & Q(valid_until__lt=timezone.now()))
126         | Q(valid_until=None)
127     ).update(last_used=date)
128     return result
129 
130 
131 def revoke_old_mandates() -> int:
132     """Revoke all mandates that have not been used for 36 months or more.
133 
134     :return: number of affected rows
135     """
136     return BankAccount.objects.filter(
137         last_used__lte=(timezone.now() - timezone.timedelta(days=36 * 30))
138     ).update(valid_until=timezone.now().date())
139 
140 
141 def process_batch(batch):
142     """Process a Thalia Pay batch.
143 
144     :param batch: the batch to be processed
145     :return:
146     """
147     batch.processed = True
148 
149     payments = batch.payments_set.select_related("paid_by")
150     for payment in payments:
151         bank_account = payment.paid_by.bank_accounts.last()
152         bank_account.last_used = batch.withdrawal_date
153         bank_account.save()
154 
155     batch.save()
156 
157     send_tpay_batch_processing_emails(batch)
158 
159 
160 def derive_next_mandate_no(member) -> str:
161     accounts = (
162         BankAccount.objects.filter(owner=PaymentUser.objects.get(pk=member.pk))
163         .exclude(mandate_no=None)
164         .filter(mandate_no__regex=BankAccount.MANDATE_NO_DEFAULT_REGEX)
165     )
166     new_mandate_no = 1 + max(
167         (int(account.mandate_no.split("-")[1]) for account in accounts), default=0
168     )
169     return f"{member.pk}-{new_mandate_no}"
170 
171 
172 def send_tpay_batch_processing_emails(batch):
173     """Send withdrawal notice emails to all members in a batch."""
174     member_payments = batch.payments_set.values("paid_by").annotate(total=Sum("amount"))
175     for member_row in member_payments:
176         member = PaymentUser.objects.get(pk=member_row["paid_by"])
177         total_amount = member_row["total"]
178 
179         send_email(
180             member.email,
181             _("Thalia Pay withdrawal notice"),
182             "payments/email/tpay_withdrawal_notice_mail.txt",
183             {
184                 "name": member.get_full_name(),
185                 "batch": batch,
186                 "bank_account": member.bank_accounts.filter(
187                     mandate_no__isnull=False
188                 ).last(),
189                 "creditor_id": settings.SEPA_CREDITOR_ID,
190                 "payments": batch.payments_set.filter(paid_by=member),
191                 "total_amount": total_amount,
192                 "payments_url": (
193                     settings.BASE_URL
194                     + reverse(
195                         "payments:payment-list",
196                     )
197                 ),
198             },
199         )
200     return len(member_payments)
201 
202 
203 def execute_data_minimisation(dry_run=False):
204     """Anonymizes payments older than 7 years."""
205     # Sometimes years are 366 days of course, but better delete 1 or 2 days early than late
206     payment_deletion_period = timezone.now().date() - timezone.timedelta(days=(365 * 7))
207     bankaccount_deletion_period = timezone.now() - datetime.timedelta(days=(31 * 13))
208 
209     queryset_payments = Payment.objects.filter(
210         created_at__lte=payment_deletion_period
211     ).exclude(paid_by__isnull=True)
212 
213     # Delete bank accounts that are not valid anymore, and have not been used in the last 13 months
214     # (13 months is the required time we need to keep the mandates for)
215     queryset_bankaccounts = BankAccount.objects.all()
216     queryset_bankaccounts = queryset_bankaccounts.filter(
217         valid_until__lt=timezone.now()
218     )  # Keep valid bank accounts
219     queryset_bankaccounts = queryset_bankaccounts.exclude(  # Also keep bank accounts that
220         Q(
221             owner__paid_payment_set__type=Payment.TPAY
222         ),  # are used for Thalia Pay payments, AND
223         Q(
224             owner__paid_payment_set__batch__isnull=True
225         )  # have a payment that is in no batch, OR
226         | Q(
227             owner__paid_payment_set__batch__processed=False
228         )  # have an unprocessed batch, OR
229         | Q(
230             owner__paid_payment_set__batch__processing_date__gt=bankaccount_deletion_period  # or have a processed batch that is not older than 13 months
231         ),
232     )
233 
234     if not dry_run:
235         queryset_payments.update(paid_by=None, processed_by=None)
236         queryset_bankaccounts.delete()
237     return queryset_payments
238
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/website/payments/services.py b/website/payments/services.py
--- a/website/payments/services.py
+++ b/website/payments/services.py
@@ -149,8 +149,13 @@
     payments = batch.payments_set.select_related("paid_by")
     for payment in payments:
         bank_account = payment.paid_by.bank_accounts.last()
-        bank_account.last_used = batch.withdrawal_date
-        bank_account.save()
+        if not bank_account:  # pragma: no cover
+            # This should not happen, cannot haver, does not happen (right... ;p), but if it does, we don't want to crash, but just remove the payment from the batch (make it unprocessed)
+            payment.batch = None
+            payment.save()
+        else:
+            bank_account.last_used = batch.withdrawal_date
+            bank_account.save()
 
     batch.save()
 
@@ -215,7 +220,7 @@
     queryset_bankaccounts = BankAccount.objects.all()
     queryset_bankaccounts = queryset_bankaccounts.filter(
         valid_until__lt=timezone.now()
-    )  # Keep valid bank accounts
+    )  # We must always keep valid bank accounts. so we only select the ones that are not valid anymore (valid_until < now)
     queryset_bankaccounts = queryset_bankaccounts.exclude(  # Also keep bank accounts that
         Q(
             owner__paid_payment_set__type=Payment.TPAY

{"golden_diff": "diff --git a/website/payments/services.py b/website/payments/services.py\n--- a/website/payments/services.py\n+++ b/website/payments/services.py\n@@ -149,8 +149,13 @@\n     payments = batch.payments_set.select_related(\"paid_by\")\n     for payment in payments:\n         bank_account = payment.paid_by.bank_accounts.last()\n-        bank_account.last_used = batch.withdrawal_date\n-        bank_account.save()\n+        if not bank_account:  # pragma: no cover\n+            # This should not happen, cannot haver, does not happen (right... ;p), but if it does, we don't want to crash, but just remove the payment from the batch (make it unprocessed)\n+            payment.batch = None\n+            payment.save()\n+        else:\n+            bank_account.last_used = batch.withdrawal_date\n+            bank_account.save()\n \n     batch.save()\n \n@@ -215,7 +220,7 @@\n     queryset_bankaccounts = BankAccount.objects.all()\n     queryset_bankaccounts = queryset_bankaccounts.filter(\n         valid_until__lt=timezone.now()\n-    )  # Keep valid bank accounts\n+    )  # We must always keep valid bank accounts. so we only select the ones that are not valid anymore (valid_until < now)\n     queryset_bankaccounts = queryset_bankaccounts.exclude(  # Also keep bank accounts that\n         Q(\n             owner__paid_payment_set__type=Payment.TPAY\n", "issue": "Unprocessed Thalia Pay payment without bank account\nApparently it is possible that people either pay with Thalia pay without having a valid bank account, or, it is possible to remove a bank account after a Thalia Pay payment is made but not processed (in which case it should not be possible)\n\n\nSentry Issue: [CONCREXIT-HD](https://sentry.io/organizations/thalia/issues/3640470247/?referrer=github_integration)\n\n```\nAttributeError: 'NoneType' object has no attribute 'last_used'\n(8 additional frame(s) were not displayed)\n...\n  File \"django/utils/decorators.py\", line 46, in _wrapper\n    return bound_method(*args, **kwargs)\n  File \"django/contrib/auth/decorators.py\", line 23, in _wrapped_view\n    return view_func(request, *args, **kwargs)\n  File \"django/views/generic/base.py\", line 119, in dispatch\n    return handler(request, *args, **kwargs)\n  File \"payments/admin_views.py\", line 107, in post\n    services.process_batch(batch)\n  File \"payments/services.py\", line 151, in process_batch\n    bank_account.last_used = batch.withdrawal_date\n```\n", "before_files": [{"content": "\"\"\"The services defined by the payments package.\"\"\"\nimport datetime\nfrom typing import Union\n\nfrom django.conf import settings\nfrom django.db.models import Model, Q, QuerySet, Sum\nfrom django.urls import reverse\nfrom django.utils import timezone\nfrom django.utils.translation import gettext_lazy as _\n\nfrom members.models import Member\nfrom utils.snippets import send_email\n\nfrom .exceptions import PaymentError\nfrom .models import BankAccount, Payment, PaymentUser\nfrom .payables import Payable, payables\n\n\ndef create_payment(\n    model_payable: Union[Model, Payable],\n    processed_by: Member,\n    pay_type: Union[Payment.CASH, Payment.CARD, Payment.WIRE, Payment.TPAY],\n) -> Payment:\n    \"\"\"Create a new payment from a payable object.\n\n    :param model_payable: Payable or Model object\n    :param processed_by: PaymentUser that processed this payment\n    :param pay_type: Payment type\n    :return: Payment object\n    \"\"\"\n    if pay_type not in (Payment.CASH, Payment.CARD, Payment.WIRE, Payment.TPAY):\n        raise PaymentError(\"Invalid payment type\")\n\n    if isinstance(model_payable, Payable):\n        payable = model_payable\n    else:\n        payable = payables.get_payable(model_payable)\n\n    payer = (\n        PaymentUser.objects.get(pk=payable.payment_payer.pk)\n        if payable.payment_payer\n        else None\n    )\n\n    if not (\n        (payer and payer == processed_by and pay_type == Payment.TPAY)\n        or (payable.can_manage_payment(processed_by) and pay_type != Payment.TPAY)\n    ):\n        raise PaymentError(\n            _(\"User processing payment does not have the right permissions\")\n        )\n\n    if payable.payment_amount == 0:\n        raise PaymentError(_(\"Payment amount 0 is not accepted\"))\n\n    if pay_type == Payment.TPAY and not payer.tpay_enabled:\n        raise PaymentError(_(\"This user does not have Thalia Pay enabled\"))\n\n    if not payable.paying_allowed:\n        raise PaymentError(_(\"Payment restricted\"))\n\n    if payable.payment is not None:\n        payable.payment.amount = payable.payment_amount\n        payable.payment.notes = payable.payment_notes\n        payable.payment.topic = payable.payment_topic\n        payable.payment.paid_by = payer\n        payable.payment.processed_by = processed_by\n        payable.payment.type = pay_type\n        payable.payment.save()\n    else:\n        payable.payment = Payment.objects.create(\n            processed_by=processed_by,\n            amount=payable.payment_amount,\n            notes=payable.payment_notes,\n            topic=payable.payment_topic,\n            paid_by=payer,\n            type=pay_type,\n        )\n    return payable.payment\n\n\ndef delete_payment(model: Model, member: Member = None, ignore_change_window=False):\n    \"\"\"Remove a payment from a payable object.\n\n    :param model: Payable or Model object\n    :param member: member deleting the payment\n    :param ignore_change_window: ignore the payment change window\n    :return:\n    \"\"\"\n    payable = payables.get_payable(model)\n\n    if member and not payable.can_manage_payment(member):\n        raise PaymentError(\n            _(\"User deleting payment does not have the right permissions.\")\n        )\n\n    payment = payable.payment\n    if (\n        payment.created_at\n        < timezone.now() - timezone.timedelta(seconds=settings.PAYMENT_CHANGE_WINDOW)\n        and not ignore_change_window\n    ):\n        raise PaymentError(_(\"This payment cannot be deleted anymore.\"))\n    if payment.batch and payment.batch.processed:\n        raise PaymentError(\n            _(\"This payment has already been processed and hence cannot be deleted.\")\n        )\n\n    payable.payment = None\n    payable.model.save()\n    payment.delete()\n\n\ndef update_last_used(queryset: QuerySet, date: datetime.date = None) -> int:\n    \"\"\"Update the last used field of a BankAccount queryset.\n\n    :param queryset: Queryset of BankAccounts\n    :param date: date to set last_used to\n    :return: number of affected rows\n    \"\"\"\n    if not date:\n        date = timezone.now().date()\n\n    result = queryset.filter(\n        (Q(valid_from__gte=timezone.now()) & Q(valid_until__lt=timezone.now()))\n        | Q(valid_until=None)\n    ).update(last_used=date)\n    return result\n\n\ndef revoke_old_mandates() -> int:\n    \"\"\"Revoke all mandates that have not been used for 36 months or more.\n\n    :return: number of affected rows\n    \"\"\"\n    return BankAccount.objects.filter(\n        last_used__lte=(timezone.now() - timezone.timedelta(days=36 * 30))\n    ).update(valid_until=timezone.now().date())\n\n\ndef process_batch(batch):\n    \"\"\"Process a Thalia Pay batch.\n\n    :param batch: the batch to be processed\n    :return:\n    \"\"\"\n    batch.processed = True\n\n    payments = batch.payments_set.select_related(\"paid_by\")\n    for payment in payments:\n        bank_account = payment.paid_by.bank_accounts.last()\n        bank_account.last_used = batch.withdrawal_date\n        bank_account.save()\n\n    batch.save()\n\n    send_tpay_batch_processing_emails(batch)\n\n\ndef derive_next_mandate_no(member) -> str:\n    accounts = (\n        BankAccount.objects.filter(owner=PaymentUser.objects.get(pk=member.pk))\n        .exclude(mandate_no=None)\n        .filter(mandate_no__regex=BankAccount.MANDATE_NO_DEFAULT_REGEX)\n    )\n    new_mandate_no = 1 + max(\n        (int(account.mandate_no.split(\"-\")[1]) for account in accounts), default=0\n    )\n    return f\"{member.pk}-{new_mandate_no}\"\n\n\ndef send_tpay_batch_processing_emails(batch):\n    \"\"\"Send withdrawal notice emails to all members in a batch.\"\"\"\n    member_payments = batch.payments_set.values(\"paid_by\").annotate(total=Sum(\"amount\"))\n    for member_row in member_payments:\n        member = PaymentUser.objects.get(pk=member_row[\"paid_by\"])\n        total_amount = member_row[\"total\"]\n\n        send_email(\n            member.email,\n            _(\"Thalia Pay withdrawal notice\"),\n            \"payments/email/tpay_withdrawal_notice_mail.txt\",\n            {\n                \"name\": member.get_full_name(),\n                \"batch\": batch,\n                \"bank_account\": member.bank_accounts.filter(\n                    mandate_no__isnull=False\n                ).last(),\n                \"creditor_id\": settings.SEPA_CREDITOR_ID,\n                \"payments\": batch.payments_set.filter(paid_by=member),\n                \"total_amount\": total_amount,\n                \"payments_url\": (\n                    settings.BASE_URL\n                    + reverse(\n                        \"payments:payment-list\",\n                    )\n                ),\n            },\n        )\n    return len(member_payments)\n\n\ndef execute_data_minimisation(dry_run=False):\n    \"\"\"Anonymizes payments older than 7 years.\"\"\"\n    # Sometimes years are 366 days of course, but better delete 1 or 2 days early than late\n    payment_deletion_period = timezone.now().date() - timezone.timedelta(days=(365 * 7))\n    bankaccount_deletion_period = timezone.now() - datetime.timedelta(days=(31 * 13))\n\n    queryset_payments = Payment.objects.filter(\n        created_at__lte=payment_deletion_period\n    ).exclude(paid_by__isnull=True)\n\n    # Delete bank accounts that are not valid anymore, and have not been used in the last 13 months\n    # (13 months is the required time we need to keep the mandates for)\n    queryset_bankaccounts = BankAccount.objects.all()\n    queryset_bankaccounts = queryset_bankaccounts.filter(\n        valid_until__lt=timezone.now()\n    )  # Keep valid bank accounts\n    queryset_bankaccounts = queryset_bankaccounts.exclude(  # Also keep bank accounts that\n        Q(\n            owner__paid_payment_set__type=Payment.TPAY\n        ),  # are used for Thalia Pay payments, AND\n        Q(\n            owner__paid_payment_set__batch__isnull=True\n        )  # have a payment that is in no batch, OR\n        | Q(\n            owner__paid_payment_set__batch__processed=False\n        )  # have an unprocessed batch, OR\n        | Q(\n            owner__paid_payment_set__batch__processing_date__gt=bankaccount_deletion_period  # or have a processed batch that is not older than 13 months\n        ),\n    )\n\n    if not dry_run:\n        queryset_payments.update(paid_by=None, processed_by=None)\n        queryset_bankaccounts.delete()\n    return queryset_payments\n", "path": "website/payments/services.py"}], "after_files": [{"content": "\"\"\"The services defined by the payments package.\"\"\"\nimport datetime\nfrom typing import Union\n\nfrom django.conf import settings\nfrom django.db.models import Model, Q, QuerySet, Sum\nfrom django.urls import reverse\nfrom django.utils import timezone\nfrom django.utils.translation import gettext_lazy as _\n\nfrom members.models import Member\nfrom utils.snippets import send_email\n\nfrom .exceptions import PaymentError\nfrom .models import BankAccount, Payment, PaymentUser\nfrom .payables import Payable, payables\n\n\ndef create_payment(\n    model_payable: Union[Model, Payable],\n    processed_by: Member,\n    pay_type: Union[Payment.CASH, Payment.CARD, Payment.WIRE, Payment.TPAY],\n) -> Payment:\n    \"\"\"Create a new payment from a payable object.\n\n    :param model_payable: Payable or Model object\n    :param processed_by: PaymentUser that processed this payment\n    :param pay_type: Payment type\n    :return: Payment object\n    \"\"\"\n    if pay_type not in (Payment.CASH, Payment.CARD, Payment.WIRE, Payment.TPAY):\n        raise PaymentError(\"Invalid payment type\")\n\n    if isinstance(model_payable, Payable):\n        payable = model_payable\n    else:\n        payable = payables.get_payable(model_payable)\n\n    payer = (\n        PaymentUser.objects.get(pk=payable.payment_payer.pk)\n        if payable.payment_payer\n        else None\n    )\n\n    if not (\n        (payer and payer == processed_by and pay_type == Payment.TPAY)\n        or (payable.can_manage_payment(processed_by) and pay_type != Payment.TPAY)\n    ):\n        raise PaymentError(\n            _(\"User processing payment does not have the right permissions\")\n        )\n\n    if payable.payment_amount == 0:\n        raise PaymentError(_(\"Payment amount 0 is not accepted\"))\n\n    if pay_type == Payment.TPAY and not payer.tpay_enabled:\n        raise PaymentError(_(\"This user does not have Thalia Pay enabled\"))\n\n    if not payable.paying_allowed:\n        raise PaymentError(_(\"Payment restricted\"))\n\n    if payable.payment is not None:\n        payable.payment.amount = payable.payment_amount\n        payable.payment.notes = payable.payment_notes\n        payable.payment.topic = payable.payment_topic\n        payable.payment.paid_by = payer\n        payable.payment.processed_by = processed_by\n        payable.payment.type = pay_type\n        payable.payment.save()\n    else:\n        payable.payment = Payment.objects.create(\n            processed_by=processed_by,\n            amount=payable.payment_amount,\n            notes=payable.payment_notes,\n            topic=payable.payment_topic,\n            paid_by=payer,\n            type=pay_type,\n        )\n    return payable.payment\n\n\ndef delete_payment(model: Model, member: Member = None, ignore_change_window=False):\n    \"\"\"Remove a payment from a payable object.\n\n    :param model: Payable or Model object\n    :param member: member deleting the payment\n    :param ignore_change_window: ignore the payment change window\n    :return:\n    \"\"\"\n    payable = payables.get_payable(model)\n\n    if member and not payable.can_manage_payment(member):\n        raise PaymentError(\n            _(\"User deleting payment does not have the right permissions.\")\n        )\n\n    payment = payable.payment\n    if (\n        payment.created_at\n        < timezone.now() - timezone.timedelta(seconds=settings.PAYMENT_CHANGE_WINDOW)\n        and not ignore_change_window\n    ):\n        raise PaymentError(_(\"This payment cannot be deleted anymore.\"))\n    if payment.batch and payment.batch.processed:\n        raise PaymentError(\n            _(\"This payment has already been processed and hence cannot be deleted.\")\n        )\n\n    payable.payment = None\n    payable.model.save()\n    payment.delete()\n\n\ndef update_last_used(queryset: QuerySet, date: datetime.date = None) -> int:\n    \"\"\"Update the last used field of a BankAccount queryset.\n\n    :param queryset: Queryset of BankAccounts\n    :param date: date to set last_used to\n    :return: number of affected rows\n    \"\"\"\n    if not date:\n        date = timezone.now().date()\n\n    result = queryset.filter(\n        (Q(valid_from__gte=timezone.now()) & Q(valid_until__lt=timezone.now()))\n        | Q(valid_until=None)\n    ).update(last_used=date)\n    return result\n\n\ndef revoke_old_mandates() -> int:\n    \"\"\"Revoke all mandates that have not been used for 36 months or more.\n\n    :return: number of affected rows\n    \"\"\"\n    return BankAccount.objects.filter(\n        last_used__lte=(timezone.now() - timezone.timedelta(days=36 * 30))\n    ).update(valid_until=timezone.now().date())\n\n\ndef process_batch(batch):\n    \"\"\"Process a Thalia Pay batch.\n\n    :param batch: the batch to be processed\n    :return:\n    \"\"\"\n    batch.processed = True\n\n    payments = batch.payments_set.select_related(\"paid_by\")\n    for payment in payments:\n        bank_account = payment.paid_by.bank_accounts.last()\n        if not bank_account:  # pragma: no cover\n            # This should not happen, cannot haver, does not happen (right... ;p), but if it does, we don't want to crash, but just remove the payment from the batch (make it unprocessed)\n            payment.batch = None\n            payment.save()\n        else:\n            bank_account.last_used = batch.withdrawal_date\n            bank_account.save()\n\n    batch.save()\n\n    send_tpay_batch_processing_emails(batch)\n\n\ndef derive_next_mandate_no(member) -> str:\n    accounts = (\n        BankAccount.objects.filter(owner=PaymentUser.objects.get(pk=member.pk))\n        .exclude(mandate_no=None)\n        .filter(mandate_no__regex=BankAccount.MANDATE_NO_DEFAULT_REGEX)\n    )\n    new_mandate_no = 1 + max(\n        (int(account.mandate_no.split(\"-\")[1]) for account in accounts), default=0\n    )\n    return f\"{member.pk}-{new_mandate_no}\"\n\n\ndef send_tpay_batch_processing_emails(batch):\n    \"\"\"Send withdrawal notice emails to all members in a batch.\"\"\"\n    member_payments = batch.payments_set.values(\"paid_by\").annotate(total=Sum(\"amount\"))\n    for member_row in member_payments:\n        member = PaymentUser.objects.get(pk=member_row[\"paid_by\"])\n        total_amount = member_row[\"total\"]\n\n        send_email(\n            member.email,\n            _(\"Thalia Pay withdrawal notice\"),\n            \"payments/email/tpay_withdrawal_notice_mail.txt\",\n            {\n                \"name\": member.get_full_name(),\n                \"batch\": batch,\n                \"bank_account\": member.bank_accounts.filter(\n                    mandate_no__isnull=False\n                ).last(),\n                \"creditor_id\": settings.SEPA_CREDITOR_ID,\n                \"payments\": batch.payments_set.filter(paid_by=member),\n                \"total_amount\": total_amount,\n                \"payments_url\": (\n                    settings.BASE_URL\n                    + reverse(\n                        \"payments:payment-list\",\n                    )\n                ),\n            },\n        )\n    return len(member_payments)\n\n\ndef execute_data_minimisation(dry_run=False):\n    \"\"\"Anonymizes payments older than 7 years.\"\"\"\n    # Sometimes years are 366 days of course, but better delete 1 or 2 days early than late\n    payment_deletion_period = timezone.now().date() - timezone.timedelta(days=(365 * 7))\n    bankaccount_deletion_period = timezone.now() - datetime.timedelta(days=(31 * 13))\n\n    queryset_payments = Payment.objects.filter(\n        created_at__lte=payment_deletion_period\n    ).exclude(paid_by__isnull=True)\n\n    # Delete bank accounts that are not valid anymore, and have not been used in the last 13 months\n    # (13 months is the required time we need to keep the mandates for)\n    queryset_bankaccounts = BankAccount.objects.all()\n    queryset_bankaccounts = queryset_bankaccounts.filter(\n        valid_until__lt=timezone.now()\n    )  # We must always keep valid bank accounts. so we only select the ones that are not valid anymore (valid_until < now)\n    queryset_bankaccounts = queryset_bankaccounts.exclude(  # Also keep bank accounts that\n        Q(\n            owner__paid_payment_set__type=Payment.TPAY\n        ),  # are used for Thalia Pay payments, AND\n        Q(\n            owner__paid_payment_set__batch__isnull=True\n        )  # have a payment that is in no batch, OR\n        | Q(\n            owner__paid_payment_set__batch__processed=False\n        )  # have an unprocessed batch, OR\n        | Q(\n            owner__paid_payment_set__batch__processing_date__gt=bankaccount_deletion_period  # or have a processed batch that is not older than 13 months\n        ),\n    )\n\n    if not dry_run:\n        queryset_payments.update(paid_by=None, processed_by=None)\n        queryset_bankaccounts.delete()\n    return queryset_payments\n", "path": "website/payments/services.py"}]}